[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug 报告\ndescription: 创建 Bug 报告以帮助开发者改进\ntitle: \"以简单的一段字概括你所遇到的问题\"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## 反馈须知\n        - 请务必完整填写下面的内容，如果缺少必要的信息，将无法解决任何问题\n        - 一个 issue 请只反馈一个 bug 或功能建议，一次性反馈多个不同的问题或建议或将会被直接关闭\n        - 注意你的标题，以简单的一段字概括你所遇到的问题。不要使用无意义内容或全部复制粘贴\n        - 该项目不为任何旧版本提供维护支持，请务必确认已更新到最新版本\n        - 应用仅支持系统硬件解码，如遇到播放卡顿或无法播放，请先检查设备芯片性能以及编码支持情况\n\n  - type: textarea\n    id: description\n    validations:\n      required: true\n    attributes:\n      label: Bug 描述\n      description: 请简短地描述你遇到的问题\n  - type: textarea\n    id: steps\n    validations:\n      required: true\n    attributes:\n      label: 复现问题的步骤\n      render: plain text\n      description: 请提供复现问题的步骤，如果不能，请写明原因\n      placeholder: |\n        示例步骤:\n        1. 进入 '...'\n        2. 点击 '....'\n        3. 滚动到 '....'\n        4. 出现问题\n  - type: textarea\n    id: expected-behavior\n    validations:\n      required: true\n    attributes:\n      label: 预期行为\n      description: 简要描述你希望看到什么样的结果\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: 截图\n      description: 如果可以，提交截图更有助于我们分析问题\n  - type: dropdown\n    id: app-version-confirm-use-latest\n    validations:\n      required: true\n    attributes:\n      label: 请确认已更新到如下所示的版本\n      description: |\n        ![GitHub Release Pre-Release](https://img.shields.io/endpoint?url=https%3A%2F%2Fbadge.versions.bv.aaa1115910.dev%2Fgithub%3Fprerelease%3Dtrue)\n      options:\n        - '我正在使用旧版本'\n        - '已更新到当前最新 Alpha 版'\n  - type: input\n    id: app-version\n    validations:\n      required: true\n    attributes:\n      label: 当前版本号\n      placeholder: 0.0.1.r29.a6d7ecb.release (或使用缩写例如 r29)\n  - type: input\n    id: android-version\n    validations:\n      required: true\n    attributes:\n      label: Android 版本\n      placeholder: Android 13\n  - type: input\n    id: device-info\n    attributes:\n      label: 设备厂商及型号\n      placeholder: Sony - BRAVIA XR MASTER SERIES Z9K\n  - type: input\n    id: video\n    attributes:\n      label: 遇到问题的视频 avid 或 bvid\n      placeholder: av170001\n  - type: textarea\n    id: additional-logs\n    attributes:\n      label: 相关日志\n      description: |\n        你可以在 `设置` > `更多设置` > `查看日志` 中查看已保存的日志，扫码即可下载获取（在同一网络环境下）\n        在日志列表中可找到自动生成的崩溃日志，或可在功能遇到问题（例如加载失败）后手动创建日志文件\n        上传文件时请务必等待文件上传完成后再提交 issue\n  - type: textarea\n    id: additional-content\n    attributes:\n      label: 附加信息\n      description: 添加你认为有必要的信息，例如出现问题的相关视频等等\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 功能需求\ndescription: 为项目提供建议\ntitle: \"以简单的一段字概括你的建议\"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## 反馈须知\n        - 请务必完整填写下面的内容，如果缺少必要的信息，将无法解决任何问题\n        - 一个 issue 请只反馈一个 bug 或功能建议，一次性反馈多个不同的问题或建议或将会被直接关闭\n        - 注意你的标题，以简单的一段字概括你所遇到的问题。不要使用无意义内容或全部复制粘贴\n        - 提出建议也不一定会做，这不可能是万能的许愿机，如果自己确实想要，建议 fork 项目自己实现\n        - 该项目不为任何旧版本提供维护支持，请务必确认已更新到最新版本\n\n  - type: textarea\n    id: problem-description\n    validations:\n      required: true\n    attributes:\n      label: 问题描述\n      description: 描述你想要解决的问题或者功能的使用场景\n      placeholder: 请描述你遇到的问题或者链接到已存在的 issue\n  - type: textarea\n    id: solution-description\n    validations:\n      required: true\n    attributes:\n      label: 描述解决方案\n      description: 清晰简明地描述你所想要发生的事情，即解决方案\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: 描述备选方案\n      description: 解决该问题的备选方案\n  - type: textarea\n    id: additional-info\n    attributes:\n      label: 附加信息\n      description: 添加你认为有必要的信息\n"
  },
  {
    "path": ".github/workflows/alpha.yml",
    "content": "name: Alpha Build\n\non:\n  push:\n    branches:\n      - develop\n\njobs:\n  build-alpha:\n    name: Build Alpha Apk\n    runs-on: macos-latest\n    if: github.repository == 'aaa1115910/bv'\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          ref: develop\n          fetch-depth: 0\n          submodules: 'true'\n\n      - name: Set up JDK 21\n        uses: actions/setup-java@v5\n        with:\n          java-version: '21'\n          distribution: 'temurin'\n\n      - name: Setup Gradle to generate and submit dependency graphs\n        uses: gradle/actions/setup-gradle@v3\n        with:\n          dependency-graph: generate-and-submit\n\n      - name: Write google-services.json\n        env:\n          DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}\n        run: echo $DATA > app/google-services.json\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Add signing properties\n        env:\n          SIGNING_PROPERTIES: ${{ secrets.SIGNING_PROPERTIES }}\n        run: |\n          echo ${{ secrets.SIGNING_PROPERTIES }} > encoded_signing_properties\n          base64 -Dd -i encoded_signing_properties > signing.properties\n\n      - name: Add jks file\n        run: |\n          echo ${{ secrets.SIGN_KEY }} > ./encoded_key\n          base64 -Dd -i encoded_key > key.jks\n\n      - name: Build apk\n        run: ./gradlew assembleDefaultAlpha assembleDefaultDebug\n\n      - name: Read alpha apk output metadata\n        id: apk-meta-alpha\n        uses: juliangruber/read-file-action@v1\n        with:\n          path: app/build/outputs/apk/default/alpha/output-metadata.json\n\n      - name: Read alpha debug apk output metadata\n        id: apk-meta-alpha-debug\n        uses: juliangruber/read-file-action@v1\n        with:\n          path: app/build/outputs/apk/default/debug/output-metadata.json\n\n      - name: Parse apk infos\n        id: apk-infos\n        run: |\n          echo \"alpha_info_version_code=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionCode }}\" >> $GITHUB_ENV\n          echo \"alpha_info_version_name=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionName }}\" >> $GITHUB_ENV\n          echo \"alpha_debug_info_version_code=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionCode }}\" >> $GITHUB_ENV\n          echo \"alpha_debug_info_version_name=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionName }}\" >> $GITHUB_ENV\n\n      - name: Determine tag name\n        id: tag_name\n        run: echo \"tag_name=alpha-r${{ env.alpha_info_version_code }}\" >> $GITHUB_ENV\n\n      - name: Get changelog\n        id: changelog\n        run: |\n          {\n            echo \"changelog<<EOF\"\n            echo \"$(git log --pretty=format:\"- %s (%h)\" ${{ github.event.before }}..${{ github.sha }})\"\n            echo \"EOF\"\n          } >> \"$GITHUB_ENV\"\n\n      # upload artifacts alpha debug\n\n      - name: Archive alpha debug build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: Alpha debug build artifact\n          path: app/build/outputs/apk/default/debug/BV_${{ env.alpha_debug_info_version_code }}_${{ env.alpha_debug_info_version_name }}_default_universal.apk\n\n      # upload artifacts alpha\n\n      - name: Archive default alpha build mappings\n        uses: actions/upload-artifact@v4\n        with:\n          name: Alpha build mappings\n          path: app/build/outputs/mapping/defaultAlpha\n\n      - name: Archive alpha build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: Alpha build artifact\n          path: app/build/outputs/apk/default/alpha/BV_${{ env.alpha_info_version_code }}_${{ env.alpha_info_version_name }}_default_universal.apk\n\n      # zip mapping because softprops/action-gh-release can't upload folder\n\n      - name: Zip mapping\n        run: zip -rj mapping.zip app/build/outputs/mapping/defaultAlpha\n\n      # upload to github release\n\n      - name: Publish Pre-Release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            app/build/outputs/apk/default/debug/BV_${{ env.alpha_debug_info_version_code }}_${{ env.alpha_debug_info_version_name }}_default_universal.apk\n            app/build/outputs/apk/default/alpha/BV_${{ env.alpha_info_version_code }}_${{ env.alpha_info_version_name }}_default_universal.apk\n            mapping.zip\n          tag_name: ${{ env.tag_name }}\n          name: ${{ env.alpha_info_version_name }}\n          prerelease: true\n          body: ${{ env.changelog }}\n          target_commitish: ${{ github.sha }}\n"
  },
  {
    "path": ".github/workflows/alpha_build_manually_without_sign.yml",
    "content": "name: Alpha Build Manually (Without signature)\n\non:\n  workflow_dispatch:\n    inputs:\n      google_services_json:\n        description: \"google-services.json (optional)\"\n\njobs:\n  build-alpha:\n\n    runs-on: macos-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          ref: develop\n          fetch-depth: 0\n          submodules: 'true'\n\n      - name: Set up JDK 21\n        uses: actions/setup-java@v5\n        with:\n          java-version: '21'\n          distribution: 'temurin'\n\n      - name: Write google-services.json\n        env:\n          DATA: ${{ github.event.inputs.google_services_json }}\n        run: echo $DATA > app/google-services.json\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Build apk\n        run: ./gradlew assembleDefaultAlpha assembleDefaultDebug\n\n      - name: Read alpha apk output metadata\n        id: apk-meta-alpha\n        uses: juliangruber/read-file-action@v1\n        with:\n          path: app/build/outputs/apk/default/alpha/output-metadata.json\n\n      - name: Read alpha debug apk output metadata\n        id: apk-meta-alpha-debug\n        uses: juliangruber/read-file-action@v1\n        with:\n          path: app/build/outputs/apk/default/debug/output-metadata.json\n\n      - name: Parse apk infos\n        id: apk-infos\n        run: |\n          echo \"alpha_info_version_code=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionCode }}\" >> $GITHUB_ENV\n          echo \"alpha_info_version_name=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionName }}\" >> $GITHUB_ENV\n          echo \"alpha_debug_info_version_code=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionCode }}\" >> $GITHUB_ENV\n          echo \"alpha_debug_info_version_name=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionName }}\" >> $GITHUB_ENV\n\n      # upload artifacts default-debug\n\n      - name: Archive default debug build artifacts (universal)\n        uses: actions/upload-artifact@v4\n        with:\n          name: Default debug build artifact (universal)\n          path: app/build/outputs/apk/default/debug/BV_${{ env.alpha_debug_info_version_code }}_${{ env.alpha_debug_info_version_name }}_default_universal.apk\n\n      # upload artifacts default-alpha\n\n      - name: Archive default alpha build mappings\n        uses: actions/upload-artifact@v4\n        with:\n          name: Default alpha build mappings\n          path: app/build/outputs/mapping/defaultAlpha\n\n      - name: Archive default alpha build artifacts (universal)\n        uses: actions/upload-artifact@v4\n        with:\n          name: Default alpha build artifact (universal)\n          path: app/build/outputs/apk/default/alpha/BV_${{ env.alpha_info_version_code }}_${{ env.alpha_info_version_name }}_default_universal.apk\n"
  },
  {
    "path": ".github/workflows/auto_close_issues.yml",
    "content": "name: Check Issues\n\non:\n  issues:\n    types: [ opened ]\njobs:\n  check:\n    runs-on: ubuntu-latest\n    steps:\n      - if: contains(github.event.issue.title, '以简单的一段字概括' )\n        id: close-invalid-title\n        name: Close Issue (invalid title)\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'add-labels,close-issue'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          labels: '无效'\n          close-reason: 'not_planned'\n\n      - if: contains(github.event.issue.body, '我正在使用旧版本' )\n        id: close-old-version\n        name: Close Issue (old version)\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'create-comment,close-issue'\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          close-reason: 'not_planned'\n          body: 请先尝试使用当前最新 alpha（或release） 版本，如果问题依然存在再提交 issue\n"
  },
  {
    "path": ".github/workflows/close_inactive_issues.yml",
    "content": "name: Close inactive issues\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n\njobs:\n  close-issues:\n    name: Close inactive issues\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      - uses: actions/stale@v5\n        with:\n          days-before-issue-stale: 60\n          days-before-issue-close: 14\n          days-before-pr-stale: -1\n          stale-issue-label: \"过时\"\n          stale-issue-message: \"该 issue 已过时，因为它已经超过 60 天没有任何活动\"\n          close-issue-message: \"该 issue 已关闭，因为它在被标记为过时后 14 天依旧没有任何活动\"\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          exempt-issue-labels: \"bug,新功能,优化,有待讨论,疑难杂症\""
  },
  {
    "path": ".github/workflows/features.yml",
    "content": "name: Feature Build\n\non:\n  push:\n    branches:\n      - 'feature/**'\n\njobs:\n  build-alpha:\n    name: Build Feature Apk\n    runs-on: macos-latest\n    if: github.repository == 'aaa1115910/bv'\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ github.ref }}\n          fetch-depth: 0\n          submodules: 'true'\n\n      - name: Set up JDK 21\n        uses: actions/setup-java@v5\n        with:\n          java-version: '21'\n          distribution: 'temurin'\n\n      - name: Write google-services.json\n        env:\n          DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}\n        run: echo $DATA > app/google-services.json\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Add signing properties\n        env:\n          SIGNING_PROPERTIES: ${{ secrets.SIGNING_PROPERTIES }}\n        run: |\n          echo ${{ secrets.SIGNING_PROPERTIES }} > encoded_signing_properties\n          base64 -Dd -i encoded_signing_properties > signing.properties\n\n      - name: Add jks file\n        run: |\n          echo ${{ secrets.SIGN_KEY }} > ./encoded_key\n          base64 -Dd -i encoded_key > key.jks\n\n      - name: Build apk\n        run: ./gradlew assembleDefaultAlpha assembleDefaultDebug\n\n      - name: Read alpha apk output metadata\n        id: apk-meta-alpha\n        uses: juliangruber/read-file-action@v1\n        with:\n          path: app/build/outputs/apk/default/alpha/output-metadata.json\n\n      - name: Read alpha debug apk output metadata\n        id: apk-meta-alpha-debug\n        uses: juliangruber/read-file-action@v1\n        with:\n          path: app/build/outputs/apk/default/debug/output-metadata.json\n\n      - name: Parse apk infos\n        id: apk-infos\n        run: |\n          echo \"alpha_info_version_code=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionCode }}\" >> $GITHUB_ENV\n          echo \"alpha_info_version_name=${{ fromJson(steps.apk-meta-alpha.outputs.content).elements[0].versionName }}\" >> $GITHUB_ENV\n          echo \"alpha_debug_info_version_code=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionCode }}\" >> $GITHUB_ENV\n          echo \"alpha_debug_info_version_name=${{ fromJson(steps.apk-meta-alpha-debug.outputs.content).elements[0].versionName }}\" >> $GITHUB_ENV\n\n      # upload artifacts alpha debug\n\n      - name: Archive alpha debug build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: Alpha debug build artifact\n          path: app/build/outputs/apk/default/debug/BV_${{ env.alpha_debug_info_version_code }}_${{ env.alpha_debug_info_version_name }}_default_universal.apk\n\n      # upload artifacts alpha\n\n      - name: Archive alpha build mappings\n        uses: actions/upload-artifact@v4\n        with:\n          name: Alpha build mappings\n          path: app/build/outputs/mapping/defaultAlpha\n\n      - name: Archive alpha build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: Alpha build artifact\n          path: app/build/outputs/apk/default/alpha/BV_${{ env.alpha_info_version_code }}_${{ env.alpha_info_version_name }}_default_universal.apk\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release Build\n\non:\n  push:\n    tags:\n      - 'v[0-9]+.[0-9]+.[0-9]+'\n      - 'v[0-9]+.[0-9]+.[0-9]+.[0-9]+'\n\njobs:\n  build-release:\n    name: Build Release Apk\n    runs-on: macos-latest\n    if: github.repository == 'aaa1115910/bv'\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          ref: master\n          fetch-depth: 0\n          submodules: 'true'\n\n      - name: Set up JDK 21\n        uses: actions/setup-java@v5\n        with:\n          java-version: '21'\n          distribution: 'temurin'\n\n      - name: Write google-services.json\n        env:\n          DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}\n        run: echo $DATA > app/google-services.json\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Add signing properties\n        env:\n          SIGNING_PROPERTIES: ${{ secrets.SIGNING_PROPERTIES }}\n        run: |\n          echo ${{ secrets.SIGNING_PROPERTIES }} > encoded_signing_properties\n          base64 -Dd -i encoded_signing_properties > signing.properties\n\n      - name: Add jks file\n        run: |\n          echo ${{ secrets.SIGN_KEY }} > ./encoded_key\n          base64 -Dd -i encoded_key > key.jks\n\n      - name: Build apk\n        run: ./gradlew assembleDefaultRelease assembleDefaultDebug\n\n      - name: Read release apk output metadata\n        id: apk-meta-release\n        uses: juliangruber/read-file-action@v1\n        with:\n          path: app/build/outputs/apk/default/release/output-metadata.json\n\n      - name: Read debug apk output metadata\n        id: apk-meta-release-debug\n        uses: juliangruber/read-file-action@v1\n        with:\n          path: app/build/outputs/apk/default/debug/output-metadata.json\n\n      - name: Parse apk infos\n        id: apk-infos\n        run: |\n          echo \"release_info_version_code=${{ fromJson(steps.apk-meta-release.outputs.content).elements[0].versionCode }}\" >> $GITHUB_ENV\n          echo \"release_info_version_name=${{ fromJson(steps.apk-meta-release.outputs.content).elements[0].versionName }}\" >> $GITHUB_ENV\n          echo \"release_debug_info_version_code=${{ fromJson(steps.apk-meta-release-debug.outputs.content).elements[0].versionCode }}\" >> $GITHUB_ENV\n          echo \"release_debug_info_version_name=${{ fromJson(steps.apk-meta-release-debug.outputs.content).elements[0].versionName }}\" >> $GITHUB_ENV\n\n      - name: Get changelog\n        id: changelog\n        run: |\n          {\n            echo \"changelog<<EOF\"\n            echo \"$(git log --pretty=format:\"- %s (%h)\" ${{ github.event.before }}..${{ github.sha }})\"\n            echo \"EOF\"\n          } >> \"$GITHUB_ENV\"\n\n      # upload artifacts release debug\n\n      - name: Archive release debug build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: Release debug build artifact\n          path: app/build/outputs/apk/default/debug/BV_${{ env.release_debug_info_version_code }}_${{ env.release_debug_info_version_name }}_default_universal.apk\n\n      # upload artifacts release\n\n      - name: Archive release build mappings\n        uses: actions/upload-artifact@v4\n        with:\n          name: Release build mappings\n          path: app/build/outputs/mapping/defaultRelease\n\n      - name: Archive release build artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: Release build artifact\n          path: app/build/outputs/apk/default/release/BV_${{ env.release_info_version_code }}_${{ env.release_info_version_name }}_default_universal.apk\n\n      # zip mapping because softprops/action-gh-release can't upload folder\n\n      - name: Zip mapping\n        run: zip -rj mapping.zip app/build/outputs/mapping/defaultRelease\n\n      # upload to github release\n\n      - name: Publish Release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            app/build/outputs/apk/default/debug/BV_${{ env.release_debug_info_version_code }}_${{ env.release_debug_info_version_name }}_default_universal.apk\n            app/build/outputs/apk/default/release/BV_${{ env.release_info_version_code }}_${{ env.release_info_version_name }}_default_universal.apk\n            mapping.zip\n          tag_name: ${{ github.ref_name }}\n          name: ${{ env.release_info_version_name }}\n          prerelease: false\n          body: ${{ env.changelog }}\n          target_commitish: ${{ github.sha }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.idea\n.gradle\n/local.properties\n.DS_Store\nbuild\n/captures\n.externalNativeBuild\n.cxx\n/signing.properties\n/.idea/jarRepositories.xml\n/.idea/migrations.xml\n/.idea/codeStyles/\n.kotlin\n.vscode\nbuildSrc\nbin\nkeystore.jks\nsigning.properties\n*.log\n/app/default/"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"libs\"]\n\tpath = libs\n\turl = https://github.com/aaa1115910/bv-libs.git\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "[![Downloads](https://img.shields.io/github/downloads/fantasytyx/bv/total?cacheSeconds=3600)](https://github.com/fantasytyx/bv/releases)\n\n- 主屏（整体框架以及首页、UGC、PGC）\n    - 重构左侧导航栏，移除抽屉展开效果\n    - 重写首页列表、UGC列表，优化性能\n    - 首页、ugc：标题改为2行、缩减视频列表的间距\n    - 首页、ugc：视频卡片显示发布时间\n    - 首页、ugc：视频卡片修改选中效果\n    - 记住每个内容页当前选中的Tab并在切换回来后恢复\n    - 支持按返回键回到左侧菜单栏\n    - 首页、ugc、pgc：顶部Tab切换增加防抖，延迟发起内容和请求\n    - 首页、ugc、pgc：简化内容区切换动画；让容器填充满屏幕，避免竖向的收缩动画\n    - 首页、ugc、pgc：按返回键定位到顶部tab时，内容区不滚动到顶部（点击tab刷新数据会回到顶部）\n    - UGC：缓存每个tab的数据，减少请求次数\n    - UGC：去掉功能并没实现的子分类\n    - 左侧菜单项未获得焦点的时候，去掉选中效果\n    - 左侧菜单切换增加防抖，延迟切换右侧内容\n    - 把“浏览历史、我的收藏、我的追番、稍后再看”整合到“首页”下面\n    - 点击左侧菜单的“用户头像”改成进入“用户中心”页面\n    - 优化各个列表焦点元素停留位置，让能显示更多完整项\n    - 解决番剧时间线页面无法显示的问题\n    - 首页的推荐、热门、动态、历史、收藏、稍后再看列表、UGC列表以及UGC视频推荐列表，UGC视频卡片增加长按确认键进入up主空间页面\n    - 动态，聚焦在视频卡片上时，按菜单键打开UP关注列表页\n    - 动态、up空间、视频推荐，在充电视频的UGC视频卡片右上角增加闪电图标（web接口）\n    - 去掉列表顶部tab的padding动画\n- 播放详情页\n    - 支持不显示UGC视频详情页，直接播放\n    - 增加点赞、投币\n    - 调整收藏、简介入口位置\n    - 修改视频封面，合集中的视频改成显示视频封面。原逻辑合集中的视频显示合集的封面\n    - 选中封面增加边框\n    - 多个收藏夹时，弹窗让用户选择要添加到哪个收藏夹，仅单个收藏夹时不弹窗直接加入默认收藏夹\n    - 视频详情页增加实例管理逻辑，最多保留3个实例，优化内存占用，同时也更少返回次数就能回到列表页\n    - 回退页面的时不请求视频关联的用户数据（点赞、投币、收藏），减少请求时有必要的\n    - 修改合集弹窗列表的序号错误\n    - 左上角视频封面图的右下角增加视频时长\n    - 视频卡片中的视频时长新增小时部分，小时部分在时长超过60分钟时出现\n    - 背景图从优先取合集封面改成取视频封面\n    - 背景图显隐增加淡入淡出动画\n- 播放页\n    - 增加“推荐视频”\n        - 操作方式： 1）双击下键; 2）按下键显示视频信息，移动焦点在底部那排按钮后再按下键\n    - 播放器控制条增加点赞、收藏、投币（仅UGC视频且要登录才会显示）\n    - 播放器控制条，默认聚焦在进度条\n        - 此时，按确认键会触发“播放/暂停”、按左右键回触发“快进/快退”\n    - 播放器控制条，增加功能按钮（播放速度、up空间、旋转画面、字幕开关、刷新当前视频、弹幕开关、播放清单、推荐视频、播放器设置、循环播放）\n        - UGC视频才会显示 up空间入口。会根据是否已关注显示不同的图标\n        - 有字幕才显示 字幕开关\n    - 新增播放器底部常驻进度条功能和配置\n    - 视频信息,调小标题字体、调小进度条高度、调浅缓冲进度颜色\n    - 播放速度调成\"画面音频\"子菜单的第一个\n    - 播放速度生效周期改成单个视频，即切视频就重置为1（原版播放速度是全局存储的）\n    - 播放速度增加倍数 2.25、2.5、2.75、3\n    - 音频编码调到画面比例前面\n    - 弹幕设置默认值改成 字体缩放110%、不透明度80%、显示区域20%\n    - 增加按下返回键隐藏控制条\n    - 增加快进/快退一段时间无操作自动确认播放\n    - 控制条中的视频时长新增小时部分，小时部分在时长超过60分钟时出现\n    - 支持“竖屏视频播放时的最大清晰度为1080P”（解决部分设备竖屏视频变形的问题）\n    - 支持“都播完后退出播放器”，退出后是回到视频详情页\n    - 支持隐藏视频播放页面左下角的视频调试信息\n    - 优化视频缓冲逻辑，缓解卡面卡死\n    - 支持当前视频播放完后不播放下一个分P视频或合集视频\n    - 给“自动播放下一个视频和自动退出”增加提示。倒数结束前可按任意键取消执行\n    - 调整标题显示样式，正确显示视频的标题（投稿名称）和副标题（分p名称）。（如果视频只有单P时不显示副标题）\n    - 修复字幕错乱bug\n    - UGC视频的视频信息增加up主名称、播放数、弹幕数、收藏数、投币数、点赞数、发布时间\n    - 新增支持切换分P/分集视频后返回到当前视频的详情页\n    - 修复非AI自动生成的字幕加载失败的问题\n    - 播放器控制条，默认聚焦在进度条，按左右键“快进/快退”、按确认键“暂停/播放”\n    - 无法播放的视频不上报历史\n    - 新增视频画面旋转功能\n- 搜索\n    - 搜索结果页视频卡片显示发布时间\n    - 支持按返回键回到左侧菜单栏\n    - 搜索结果页改成选中tab的时候才发起请求，以便减少请求次数\n- up空间页\n    - 视频卡片显示发布时间\n    - 增加 关注up 功能\n    - 修改页面历史记录，允许记录最多两个不同up的空间页\n- 收藏\n    - 视频卡片显示 收藏时间\n- 浏览历史\n    - 视频卡片显示 浏览时间\n- 标签搜索结果页\n    - 视频卡片显示发布时间\n- 设置页面\n    - 增加分类“播放设置”\n    - 界面设置-增加“首页默认标签”设置，默认“推荐”\n        - 可以修改打开应用时首页默认选中的标签\n    - 播放设置-增加是否“显示UGC视频详情页”设置，默认显示\n        - 关闭后，点击非PGC视频卡片不显示详情页直接开始播放\n    - 播放设置-增加设置“显示视频加载信息”，默认不显示\n    - 播放设置-增加设置“设置竖屏视频播放时的最大清晰度为1080P”，默认禁用\n        - 开启可解决部分设备竖屏视频变形/花屏的问题\n    - 播放设置-增加是否“自动播放下一个视频”设置，默认开启\n    - 播放设置-增加是否“都播完后退出播放器”设置，默认开启\n    - 播放设置-增加默认播放速度配置，默认1倍\n    - 播放设置-增加快进时间间隔配置，默认10秒\n    - 播放设置-增加快退时间间隔配置，默认5秒\n    - 播放设置-增加是否显示“播放器底部常驻进度条”配置，默认不显示\n    - 关于-更新，优化应用更新弹窗在内容很多时支持滚动查看\n    - 关于-更新，使用github镜像源加速下载\n- 其它\n    - 播放量改成Long类型，解决追番列表无法显示（凡人播放量超过Int类型的最大值）"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 aaa1115910\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n<img src=\"app/shared/src/main/res/drawable/ic_banner_md.webp\" style=\"border-radius: 24px; margin-top: 32px;\"/>\n\n# BV\n\n~~Bug Video~~\n\n[![Android Sdk Require](https://img.shields.io/badge/Android-6.0%2B-informational?logo=android)](https://apilevels.com/#:~:text=Jetpack%20Compose%20requires%20a%20minSdk%20of%2021%20or%20higher)\n[![GitHub](https://img.shields.io/github/license/fantasytyx/bv)](https://github.com/fantasytyx/bv)\n\n**BV 无法在中国大陆地区内的智能电视上使用，如有相关使用需求请使用 [云视听小电视](https://app.bilibili.com)**\n\n**禁止在中国境内传播、宣传、分发 BV**\n\n</div>\n\n---\nBV ~~(Bug Video)~~ 是一款 [哔哩哔哩](https://www.bilibili.com) 的第三方应用，适配 `Android 移动端`\n和 `Android TV`，使用 `Jetpack Compose` 开发\n\n**都是随心乱写的代码，能跑就行。**\n\n---\n\n<div align=\"center\">\n\n# 学废了\n\n</div>\n\n## 声明\n\n**此项目是个人为了学习安卓开发而fork, 仅用于学习和测试，禁止在中国境内传播、宣传、分发，如有相关使用需求请使用 [哔哩哔哩官方APP](https://app.bilibili.com)，否则后果自负**\n\n## 修改\n在原bv的基础上做了一些修改，包括：\n- 把“浏览历史、我的收藏、我的追番、稍后再看”整合到“首页”下面\n- 增加“首页默认标签”设置 （设置-界面设置，默认“推荐”）\n  - **可以修改打开应用时首页默认选中的标签**，选项有：推荐、热门、动态、历史、收藏、追番、稍后再看\n- 首页推荐、热门、动态、历史、收藏、稍后再看，UGC列表以及UGC视频推荐列表，可以**在UGC视频卡片长按确认键进入up主空间页面**看up的所有投稿视频\n- **动态页面**，聚焦在视频卡片上时，**按菜单键打开已关注UP列表页**，可以筛选想看的up\n- 动态、up空间、视频推荐，在充电视频的UGC视频卡片右上角增加闪电图标（web接口）\n- 在主屏右上角显示当前时间\n- 首页导航项、UGC导航项、PGC导航项支持自定义排序和隐藏\n  - 设置-界面设置\n- **添加直播**，推荐、关注、分区，直播搜索，直播弹幕\n- UGC详情页、PGC详情页、UGC&PGC视频播放页 增加评论功能\n- 支持两种导航切换模式：聚焦后自动切换、聚焦并确认才切换\n- 支持长按确认键加速播放\n- 浏览历史、收藏、追番、稍后再看 这4个列表 新增删除支持。按菜单键进入删除模式，长按（或短按）确认键删除当前选中项，按返回键退出删除模式\n\n  ![首页](https://github.com/user-attachments/assets/f621d34b-d618-4ab2-a0ef-663cc8970664)\n- **UGC视频详情页增加点赞、投币功能**\n- 增加是否“显示UGC视频详情页”设置 （默认显示）\n  - 关闭后，点击UGC视频卡片会**跳过详情页直接开始播放**\n- 合集/分P 自动滚动到最后播放的视频并高亮显示\n\n  ![UGC详情](https://github.com/user-attachments/assets/bdd6bfe7-b434-4f59-819d-49c2d002ff34)\n- **播放器页面增加“推荐视频”、“视频列表”**\n  - 操作方式： 1）双击下方向键; 2）按下键显示视频信息，移动焦点在底部那排按钮后再按下方向键\n\n  ![视频播放-推荐视频](https://github.com/user-attachments/assets/b62d1c6e-0a4f-4e39-a0c9-d3f06462d3e5)\n- **新增视频画面旋转功能**\n- 播放器控制条，**增加点赞、收藏、投币**\n  - 仅UGC视频且要登录才会显示\n- 播放器控制条，默认聚焦在进度条\n  - 此时，按确认键会触发“播放/暂停”、按左右键回触发“快进/快退”\n- 播放器控制条，增加功能按钮（播放速度、画质、up空间、画面旋转、字幕开关、重新加载当前视频、弹幕开关、循环播放、播放清单、推荐视频、视频简介、播放器设置）\n- 新增识别字幕类型，添加AI标识\n- 播放器控制条，支持设置按钮的顺序、显隐、默认焦点\n- 支持PGC视频自动跳过片头/片尾设置\n- 支持播放只有音轨的视频\n- 换成新版弹幕接口\n\n  ![视频播放](https://github.com/user-attachments/assets/8f14a371-4ff6-479e-9c25-2fe3868b1db6)\n- 调整设置，增加分类“播放设置”\n  - 把 分辨率、视频编码、音频编码、启用音频软件 4个设置移入这个分类\n  - 增加是否“显示UGC视频详情页”设置 （默认显示）\n  - 增加是否在播放页面底部 常驻“显示**迷你进度条**”设置（默认不显示）\n  - 增加“显示视频加载过程信息”设置（默认不显示）\n  - **增加“竖屏视频播放异常时的处理**方式”设置（默认不处理）\n    - 不是所有设备都有问题，没问题的同学不要开；\n    - 使用TextureView模式卡的不行的，建议用限制到1080P的模式\n  - **增加“下一个播放”设置**（默认不播放），可设置为：\n    - 不播\n    - 播推荐视频\n    - 播剧集和分P的下一个\n    - 播播剧集和分P的下一个或推荐视频\n  - 增加是否“都播完后退出播放器”设置（默认开启）\n  - 增加默认播放速度设置（默认1倍）\n  - 增加快进时间间隔设置（默认10秒）\n  - 增加快退时间间隔设置（默认5秒）\n  - 增加显示在线观看人数（默认一直显示，可选不显示、30秒后隐藏）\n  - 增加开始播放位置设置（默认从头开始播放，可选从历史位置播放）\n  - 新增PGC视频自动跳过片头/片尾设置（默认关闭）\n  - 新增 播放器控制按钮支持排序、显隐、设置默认焦点\n  - 新增 点播与直播的弹幕过滤等级设置\n  - 新增 播放器页面长按确认键的执行动作的设置，可选：打开菜单、加速播放\n  - 新增 长按确认键加速播放加速值的设置，默认2x\n- 界面设置\n  - 新增 界面模式设置，选项有：启动时自动检测、强制使用 TV，或强制使用 Mobile 界面（默认自动检测）\n  - 新增页面浏览历史相关的设置：UGC 视频详情页面的历史记录数量、详情页历史记录是否包含播放器打开的详情页、UGC 视频播放页面的历史记录数量\n  - 新增 UGC导航项设置，可修改顺序和显隐\n  - 新增 PGC导航项设置，可修改顺序和显隐\n  - 新增 直播导航项设置，可修改顺序和显隐\n  - 新增 导航切换模式设置，选项有：聚焦后自动切换、聚焦并确认才切换\n- 网络设置\n  - 新增 仅允许IPV4的选项\n\n  ![设置](https://github.com/user-attachments/assets/5e721ec3-e584-4233-a112-e7a3ee5f1afd)\n- 优化up空间页，丰富内容并增加关注功能\n- 优化已关注up列表页，增加本地搜索\n- 优化搜索页面、账号管理页面\n- 优化列表、优化视频卡片显示更多内容、精简动画、增加数据缓存、减少非必要的请求\n- 按自己的喜好调整页面的布局、元素大小、交互方式、原有功能\n- 解决一些bug等等\n\n## 构建\n自己动手丰衣足食\n- 安装开发环境\n  - Android studio、Android SDK、JAVA等等\n\n- 补全构建需要的文件\n  - 在项目根目录用使用 Android SDK 中的 keytool 工具创建签名文件 keystore.jks。\n    ```sh\n    keytool -genkey -v -keystore keystore.jks -alias 别名 -keyalg RSA -keysize 2048 -validity 10000\n    ```\n  命令说明：\n  - genkey: 生成密钥对\n  - -v: 详细输出\n  - -keystore keystore.jks: 指定生成的密钥库文件名\n  - -alias 别名: 指定密钥的别名（可以根据需要修改）\n  - -keyalg RSA: 使用 RSA 算法\n  - -keysize 2048: 密钥长度为 2048 位\n  - -validity 10000: 密钥的有效期为 10000 天（约 27 年）\n    执行此命令后，会提示你输入：\n    - 密钥库密码（keystore.pwd）\n    - 密钥密码（keystore.alias_pwd），可以与密钥库密码相同\n    - 姓名、组织单位、城市等信息，可空\n\n  - 在项目根目录增加 signing.properties 文件。文件内容如下\n    ```properties\n    keystore.path=./keystore.jks\n    keystore.pwd=创建签名文件时设置的密码\n    keystore.alias=创建签名文件时设置的别名\n    keystore.alias_pwd=创建签名文件时设置的别名密码\n    ```\n2. 执行构建命令来生成 apk 文件\n    ```sh\n    # release\n    ./gradlew clean assembleRelease\n    ```\n- 在根目录增加 signing.properties 文件。文件内容如下\n  ```properties\n  keystore.path=./keystore.jks\n  keystore.pwd=创建签名文件时设置的密码\n  keystore.alias=创建签名文件时设置的别名\n  keystore.alias_pwd=创建签名文件时设置的别名密码\n  ```\n- 执行构建命令来生成 apk 文件\n```sh\n# release\n./gradlew clean assembleRelease\n```\n\n\n## 安装\n\n### adb\n./adb.exe connect 192.168.xx.xx\n./adb.exe -s 192.168.xx.xx install -r -d {apk文件路径}\n\n### Release\n\n- [Github Release](https://github.com/fantasytyx/bv/releases)\n\n## License\n\n[MIT](LICENSE) © aaa1115910"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n/google-services.json\n/release\n/r8Test\n/debug"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nimport com.android.build.gradle.internal.api.ApkVariantOutputImpl\nimport java.io.FileInputStream\nimport java.util.Properties\n\nplugins {\n    alias(gradleLibs.plugins.android.application)\n    alias(gradleLibs.plugins.compose.compiler)\n    alias(gradleLibs.plugins.google.ksp)\n    alias(gradleLibs.plugins.kotlin.android)\n    alias(gradleLibs.plugins.kotlin.serialization)\n}\n\n\nval signingProp = file(project.rootProject.file(\"signing.properties\"))\n\nandroid {\n    signingConfigs {\n        if (signingProp.exists()) {\n            val properties = Properties().apply {\n                load(FileInputStream(signingProp))\n            }\n            create(\"key\") {\n                storeFile = rootProject.file(properties.getProperty(\"keystore.path\"))\n                storePassword = properties.getProperty(\"keystore.pwd\")\n                keyAlias = properties.getProperty(\"keystore.alias\")\n                keyPassword = properties.getProperty(\"keystore.alias_pwd\")\n            }\n        }\n    }\n\n    namespace = AppConfiguration.appId\n    compileSdk = AppConfiguration.compileSdk\n\n    defaultConfig {\n        applicationId = AppConfiguration.applicationId\n        minSdk = AppConfiguration.minSdk\n        targetSdk = AppConfiguration.targetSdk\n        versionCode = AppConfiguration.versionCode\n        versionName = AppConfiguration.versionName\n        vectorDrawables {\n            useSupportLibrary = true\n        }\n    }\n\n    flavorDimensions.add(\"channel\")\n\n    productFlavors {\n        // create(\"lite\") {\n        //     dimension = \"channel\"\n        // }\n        create(\"default\") {\n            dimension = \"channel\"\n        }\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = true\n            isShrinkResources = true // 移除未使用的资源\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n            if (signingProp.exists()) signingConfig = signingConfigs.getByName(\"key\")\n        }\n        debug {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n            applicationIdSuffix = \".debug\"\n        }\n        create(\"r8Test\") {\n            isMinifyEnabled = true\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n            applicationIdSuffix = \".r8test\"\n            if (signingProp.exists()) signingConfig = signingConfigs.getByName(\"key\")\n        }\n        create(\"alpha\") {\n            isMinifyEnabled = true\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n            if (signingProp.exists()) signingConfig = signingConfigs.getByName(\"key\")\n        }\n    }\n\n    buildFeatures {\n        compose = true\n        //buildConfig = true\n    }\n\n    packaging {\n        resources {\n            excludes += \"/META-INF/{AL2.0,LGPL2.1}\"\n            excludes += \"**/*.proto\"\n            excludes += \"**/*.kotlin_metadata\"\n            excludes += \"**/kotlin/**\"\n            excludes += \"**/*.txt\"\n            excludes += \"**/*.version\"\n        }\n\n//        if (gradle.startParameter.taskNames.find { it.startsWith(\"assembleLite\") } != null) {\n//            jniLibs {\n//                val vlcLibs = listOf(\"libvlc\", \"libc++_shared\", \"libvlcjni\")\n//                val abis = listOf(\"x86_64\", \"x86\", \"arm64-v8a\", \"armeabi-v7a\")\n//                vlcLibs.forEach { vlcLibName -> abis.forEach { abi -> excludes.add(\"lib/$abi/$vlcLibName.so\") } }\n//            }\n//        }\n    }\n\n    /*splits {\n        if (gradle.startParameter.taskNames.find { it.startsWith(\"assembleDefault\") } != null) {\n            abi {\n                isEnable = true\n                reset()\n                include(\"x86_64\", \"x86\", \"arm64-v8a\", \"armeabi-v7a\")\n                isUniversalApk = true\n            }\n        }\n    }*/\n\n    applicationVariants.configureEach {\n        val variant = this\n        outputs.configureEach {\n            (this as ApkVariantOutputImpl).apply {\n                val abi = this.filters.find { it.filterType == \"ABI\" }?.identifier ?: \"universal\"\n                outputFileName =\n                    \"BV_${AppConfiguration.versionCode}_${AppConfiguration.versionName}.${variant.buildType.name}_${variant.flavorName}_$abi.apk\"\n                versionNameOverride =\n                    \"${variant.versionName}.${variant.buildType.name}\"\n            }\n        }\n    }\n}\n\ncomposeCompiler {\n    reportsDestination = layout.buildDirectory.dir(\"compose_build_reports\")\n    stabilityConfigurationFiles.addAll(\n        layout.projectDirectory.file(\"compose_compiler_config.conf\")\n    )\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\ndependencies {\n    implementation(project(\":app:mobile\"))\n    implementation(project(\":app:tv\"))\n    implementation(project(\":app:shared\"))\n}\n\ntasks.withType<Test> {\n    useJUnitPlatform()\n}"
  },
  {
    "path": "app/compose_compiler_config.conf",
    "content": "kotlin.collections.*\nkotlin.time.Duration\n\nkotlinx.coroutines.CoroutineScope\n\nandroidx.paging.compose.LazyPagingItems\n\n# 核心稳定性\nandroidx.compose.runtime.Composable\nandroidx.compose.runtime.State\nandroidx.compose.ui.Modifier\n\n# TV 特定\nandroidx.tv.material3.Button\nandroidx.tv.material3.Card\nandroidx.tv.material3.Surface\n\n# 项目特定\ndev.aaa1115910.bv.tv.screens.main.*"
  },
  {
    "path": "app/mobile/.gitignore",
    "content": "/build"
  },
  {
    "path": "app/mobile/build.gradle.kts",
    "content": "plugins {\n    alias(gradleLibs.plugins.android.library)\n    alias(gradleLibs.plugins.compose.compiler)\n    alias(gradleLibs.plugins.google.ksp)\n    alias(gradleLibs.plugins.kotlin.android)\n    alias(gradleLibs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = AppConfiguration.appId + \".mobile\"\n    compileSdk = AppConfiguration.compileSdk\n\n    defaultConfig {\n        minSdk = AppConfiguration.minSdk\n        vectorDrawables {\n            useSupportLibrary = true\n        }\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"r8Test\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"alpha\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n\n    buildFeatures {\n        compose = true\n    }\n\n    lint {\n        targetSdk = AppConfiguration.targetSdk\n    }\n\n    testOptions {\n        targetSdk = AppConfiguration.targetSdk\n    }\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\ndependencies {\n    implementation(project(\":app:shared\"))\n}"
  },
  {
    "path": "app/mobile/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "app/mobile/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "app/mobile/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <application>\n        <activity\n            android:name=\"dev.aaa1115910.bv.mobile.activities.MainActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/Theme.BV.Mobile.Splash\">\n        </activity>\n        <activity\n            android:name=\".activities.VideoPlayerActivity\"\n            android:configChanges=\"orientation|screenSize\"\n            android:exported=\"true\"\n            android:label=\"@string/title_mobile_activity_video_player\"\n            android:theme=\"@style/Theme.BV.Mobile\" />\n        <activity\n            android:name=\".activities.LoginActivity\"\n            android:exported=\"false\"\n            android:label=\"@string/title_mobile_activity_login\"\n            android:theme=\"@style/Theme.BV.Mobile\" />\n        <activity\n            android:name=\".activities.UserSpaceActivity\"\n            android:exported=\"false\"\n            android:label=\"@string/title_mobile_activity_user_space\"\n            android:theme=\"@style/Theme.BV.Mobile\" />\n        <activity\n            android:name=\".activities.SettingsActivity\"\n            android:exported=\"false\"\n            android:label=\"@string/title_mobile_activity_settings\"\n            android:theme=\"@style/Theme.BV.Mobile\" />\n        <activity\n            android:name=\".activities.DynamicDetailActivity\"\n            android:exported=\"false\"\n            android:label=\"@string/title_mobile_activity_dynamic_detail\"\n            android:theme=\"@style/Theme.BV.Mobile\" />\n        <activity\n            android:name=\".activities.FollowingUserActivity\"\n            android:exported=\"false\"\n            android:label=\"@string/title_mobile_activity_following_user\"\n            android:theme=\"@style/Theme.BV.Mobile\" />\n        <activity\n            android:name=\".activities.HistoryActivity\"\n            android:exported=\"false\"\n            android:label=\"@string/title_mobile_activity_history\"\n            android:theme=\"@style/Theme.BV.Mobile\" />\n        <activity\n            android:name=\".activities.FavoriteActivity\"\n            android:exported=\"false\"\n            android:label=\"@string/title_mobile_activity_favorite\"\n            android:theme=\"@style/Theme.BV.Mobile\" />\n        <activity\n            android:name=\".activities.FollowingSeasonActivity\"\n            android:exported=\"false\"\n            android:label=\"@string/title_mobile_activity_following_season\"\n            android:theme=\"@style/Theme.BV.Mobile\" />\n        <activity\n            android:name=\".activities.IntentHandlerActivity\"\n            android:exported=\"true\"\n            android:label=\"@string/title_mobile_activity_intent_handler\"\n            android:launchMode=\"singleTask\"\n            android:theme=\"@style/Theme.BV.Mobile\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:host=\"qrtoken\"\n                    android:scheme=\"bugvideo\" />\n            </intent-filter>\n        </activity>\n        <activity\n            android:name=\".activities.QrTokenResultActivity\"\n            android:exported=\"false\"\n            android:label=\"@string/title_mobile_activity_qr_token_result\"\n            android:theme=\"@style/Theme.BV.Mobile\" />\n    </application>\n</manifest>"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/gallery/ImageGallery.kt",
    "content": "package com.origeek.imageViewer.gallery\n\nimport androidx.annotation.FloatRange\nimport androidx.annotation.IntRange\nimport androidx.compose.foundation.interaction.InteractionSource\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.Dp\nimport com.origeek.imageViewer.previewer.DEFAULT_ITEM_SPACE\nimport com.origeek.imageViewer.viewer.ImageViewer\nimport com.origeek.imageViewer.viewer.ImageViewerState\nimport com.origeek.imageViewer.viewer.rememberViewerState\nimport kotlinx.coroutines.launch\n\n/**\n * @program: ImageViewer\n *\n * @description:\n *\n * @author: JVZIYAOYAO\n *\n * @create: 2022-10-10 11:50\n **/\n\n/**\n * gallery手势对象\n */\nclass GalleryGestureScope(\n    // 点击事件\n    var onTap: () -> Unit = {},\n    // 双击事件\n    var onDoubleTap: () -> Boolean = { false },\n    // 长按事件\n    var onLongPress: () -> Unit = {},\n)\n\n/**\n * gallery图层对象\n */\nclass GalleryLayerScope(\n    // viewer图层\n    var viewerContainer: @Composable (\n        page: Int, viewerState: ImageViewerState, viewer: @Composable () -> Unit\n    ) -> Unit = { _, _, viewer -> viewer() },\n    // 背景图层\n    var background: @Composable ((Int) -> Unit) = {},\n    // 前景图层\n    var foreground: @Composable ((Int) -> Unit) = {},\n)\n\n/**\n * gallery状态\n */\nopen class ImageGalleryState(\n    val pagerState: ImagePagerState,\n) {\n\n    /**\n     * 当前viewer的状态\n     */\n    var imageViewerState by mutableStateOf<ImageViewerState?>(null)\n        internal set\n\n    /**\n     * 当前页码\n     */\n    val currentPage: Int\n        get() = pagerState.currentPage\n\n    /**\n     * 目标页码\n     */\n    val targetPage: Int\n        get() = pagerState.targetPage\n\n    /**\n     * interactionSource\n     */\n    val interactionSource: InteractionSource\n        get() = pagerState.interactionSource\n\n    /**\n     * 滚动到指定页面\n     * @param page Int\n     * @param pageOffset Float\n     */\n    suspend fun scrollToPage(\n        @IntRange(from = 0) page: Int,\n        @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,\n    ) = pagerState.scrollToPage(page, pageOffset)\n\n    /**\n     * 动画滚动到指定页面\n     * @param page Int\n     * @param pageOffset Float\n     */\n    suspend fun animateScrollToPage(\n        @IntRange(from = 0) page: Int,\n        @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,\n    ) = pagerState.animateScrollToPage(page, pageOffset)\n\n}\n\n/**\n * 记录gallery状态\n */\n@Composable\nfun rememberImageGalleryState(\n    @IntRange(from = 0) initialPage: Int = 0,\n    pageCount: () -> Int,\n): ImageGalleryState {\n    val imagePagerState = rememberImagePagerState(initialPage, pageCount)\n    return remember { ImageGalleryState(imagePagerState) }\n}\n\n/**\n * 图片gallery,基于Pager实现的一个图片查看列表组件\n */\n@Composable\nfun ImageGallery(\n    // 编辑参数\n    modifier: Modifier = Modifier,\n    // gallery状态\n    state: ImageGalleryState,\n    // 图片加载器\n    imageLoader: @Composable (Int) -> Any?,\n    // 每张图片之间的间隔\n    itemSpacing: Dp = DEFAULT_ITEM_SPACE,\n    // 检测手势\n    detectGesture: GalleryGestureScope.() -> Unit = {},\n    // gallery图层\n    galleryLayer: GalleryLayerScope.() -> Unit = {},\n) {\n//    require(count >= 0) { \"imageCount must be >= 0\" }\n    val scope = rememberCoroutineScope()\n    // 手势相关\n    val galleryGestureScope = remember { GalleryGestureScope() }\n    detectGesture.invoke(galleryGestureScope)\n    // 图层相关\n    val galleryLayerScope = remember { GalleryLayerScope() }\n    galleryLayer.invoke(galleryLayerScope)\n    // 确保不会越界\n    val currentPage = state.currentPage\n\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n    ) {\n        galleryLayerScope.background(currentPage)\n        ImageHorizonPager(\n            state = state.pagerState,\n            modifier = Modifier\n                .fillMaxSize(),\n            itemSpacing = itemSpacing,\n        ) { page ->\n            val imageState = rememberViewerState()\n            LaunchedEffect(key1 = currentPage) {\n                if (currentPage != page) imageState.reset()\n                if (currentPage == page) {\n                    state.imageViewerState = imageState\n                }\n            }\n            galleryLayerScope.viewerContainer(page, imageState) {\n                Box(\n                    modifier = Modifier\n                        .fillMaxSize(),\n                ) {\n                    key(page) {\n                        ImageViewer(\n                            modifier = Modifier.fillMaxSize(),\n                            model = imageLoader(page),\n                            state = imageState,\n                            boundClip = false,\n                            detectGesture = {\n                                this.onTap = {\n                                    galleryGestureScope.onTap()\n                                }\n                                this.onDoubleTap = {\n                                    val consumed = galleryGestureScope.onDoubleTap()\n                                    if (!consumed) scope.launch {\n                                        imageState.toggleScale(it)\n                                    }\n                                }\n                                this.onLongPress = { galleryGestureScope.onLongPress() }\n                            },\n                        )\n                    }\n                }\n            }\n        }\n        galleryLayerScope.foreground(currentPage)\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/gallery/ImagePager.kt",
    "content": "package com.origeek.imageViewer.gallery\n\nimport androidx.annotation.FloatRange\nimport androidx.annotation.IntRange\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.interaction.InteractionSource\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.PagerState\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n/**\n * @program: ImageViewer\n *\n * @description:\n *\n * @author: JVZIYAOYAO\n *\n * @create: 2022-10-05 21:41\n **/\n\n/**\n * 基于HorizonPager封装的pager组件\n */\nopen class ImagePagerState @OptIn(ExperimentalFoundationApi::class) constructor(\n    val pagerState: PagerState,\n) {\n\n    /**\n     * 当前页码\n     */\n    @OptIn(ExperimentalFoundationApi::class)\n    val currentPage: Int\n        get() = pagerState.currentPage\n\n    /**\n     * 目标页码\n     */\n    @OptIn(ExperimentalFoundationApi::class)\n    val targetPage: Int\n        get() = pagerState.targetPage\n\n    /**\n     * interactionSource\n     */\n    @OptIn(ExperimentalFoundationApi::class)\n    val interactionSource: InteractionSource\n        get() = pagerState.interactionSource\n\n    /**\n     * 滚动到指定页面\n     */\n    @OptIn(ExperimentalFoundationApi::class)\n    suspend fun scrollToPage(\n        // 指定的页码\n        @IntRange(from = 0) page: Int,\n        // 滚动偏移量\n        @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,\n    ) = pagerState.scrollToPage(page, pageOffset)\n\n    /**\n     * 动画滚动到指定页面\n     */\n    @OptIn(ExperimentalFoundationApi::class)\n    suspend fun animateScrollToPage(\n        // 指定的页码\n        @IntRange(from = 0) page: Int,\n        // 滚动偏移量\n        @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,\n    ) = pagerState.animateScrollToPage(page, pageOffset)\n\n}\n\n/**\n * 记录pager状态\n */\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun rememberImagePagerState(\n    // 默认显示的页码\n    @IntRange(from = 0) initialPage: Int = 0,\n    pageCount: () -> Int,\n): ImagePagerState {\n    val pageState = rememberPagerState(initialPage = initialPage, pageCount = pageCount)\n    return remember {\n        ImagePagerState(pageState)\n    }\n}\n\n/**\n * pager组件\n */\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun ImageHorizonPager(\n    // 编辑参数\n    modifier: Modifier = Modifier,\n    // pager状态\n    state: ImagePagerState,\n    // 每个item之间的间隔\n    itemSpacing: Dp = 0.dp,\n    // 页面内容\n    content: @Composable (page: Int) -> Unit,\n) {\n    HorizontalPager(\n        state = state.pagerState,\n        modifier = modifier,\n        pageSpacing = itemSpacing,\n    ) { page ->\n        content(page)\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/ImagePreviewer.kt",
    "content": "package com.origeek.imageViewer.previewer\n\nimport androidx.annotation.IntRange\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.EnterTransition\nimport androidx.compose.animation.ExitTransition\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.MutableTransitionState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.mapSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.origeek.imageViewer.gallery.GalleryGestureScope\nimport com.origeek.imageViewer.gallery.ImageGallery\nimport com.origeek.imageViewer.gallery.ImageGalleryState\nimport com.origeek.imageViewer.gallery.rememberImageGalleryState\nimport com.origeek.imageViewer.viewer.ImageViewerState\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.MainScope\n\n// 预览的默认的背景颜色\nval DEEP_DARK_FANTASY = Color(0xFF000000)\n\n// 图片间的默认间隔\nval DEFAULT_ITEM_SPACE = 12.dp\n\n// 比较轻柔的动画窗格\nval DEFAULT_SOFT_ANIMATION_SPEC = tween<Float>(320)\n\n/**\n * 预览的默认背景\n */\n@Composable\nfun DefaultPreviewerBackground() {\n    Box(\n        modifier = Modifier\n            .background(DEEP_DARK_FANTASY)\n            .fillMaxSize()\n    )\n}\n\n/**\n * 预览组件的状态\n */\nclass ImagePreviewerState(\n    // 协程作用域\n    scope: CoroutineScope = MainScope(),\n    // 默认动画窗格\n    defaultAnimationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC,\n    // 预览状态\n    galleryState: ImageGalleryState,\n) : PreviewerVerticalDragState(scope, defaultAnimationSpec, galleryState = galleryState) {\n    companion object {\n        fun getSaver(galleryState: ImageGalleryState): Saver<ImagePreviewerState, *> {\n            return mapSaver(\n                save = {\n                    mapOf<String, Any>(\n                        it::currentPage.name to it.currentPage,\n                        it::animateContainerVisibleState.name to it.animateContainerVisibleState.currentState,\n                        it::uiAlpha.name to it.uiAlpha.value,\n                        it::visible.name to it.visible,\n                    )\n                },\n                restore = {\n                    val previewerState = ImagePreviewerState(galleryState = galleryState)\n                    previewerState.animateContainerVisibleState =\n                        MutableTransitionState(it[ImagePreviewerState::animateContainerVisibleState.name] as Boolean)\n                    previewerState.uiAlpha =\n                        Animatable(it[ImagePreviewerState::uiAlpha.name] as Float)\n                    previewerState.visible = it[ImagePreviewerState::visible.name] as Boolean\n                    previewerState\n                }\n            )\n        }\n    }\n}\n\n/**\n * 记录预览组件状态\n */\n@Composable\nfun rememberPreviewerState(\n    // 协程作用域\n    scope: CoroutineScope = rememberCoroutineScope(),\n    // 动画窗格\n    animationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC,\n    // 开启垂直手势的类型\n    verticalDragType: VerticalDragType = VerticalDragType.None,\n    // 初始页码\n    @IntRange(from = 0) initialPage: Int = 0,\n    // 获取页数\n    pageCount: () -> Int,\n    // 提供给组件用于获取key的方法\n    getKey: ((Int) -> Any)? = null,\n): ImagePreviewerState {\n    val galleryState = rememberImageGalleryState(initialPage, pageCount)\n    val imagePreviewerState = rememberSaveable(saver = ImagePreviewerState.getSaver(galleryState)) {\n        ImagePreviewerState(galleryState = galleryState)\n    }\n    imagePreviewerState.scope = scope\n    imagePreviewerState.getKey = getKey\n    imagePreviewerState.defaultAnimationSpec = animationSpec\n    imagePreviewerState.verticalDragType = verticalDragType\n    return imagePreviewerState\n}\n\n/**\n * 默认的弹出预览时的动画效果\n */\nval DEFAULT_PREVIEWER_ENTER_TRANSITION =\n    scaleIn(tween(180)) + fadeIn(tween(240))\n\n/**\n * 默认的关闭预览时的动画效果\n */\nval DEFAULT_PREVIEWER_EXIT_TRANSITION =\n    scaleOut(tween(320)) + fadeOut(tween(240))\n\n// 默认淡入淡出动画窗格\nval DEFAULT_CROSS_FADE_ANIMATE_SPEC: AnimationSpec<Float> = tween(80)\n\n// 加载占位默认的进入动画\nval DEFAULT_PLACEHOLDER_ENTER_TRANSITION = fadeIn(tween(200))\n\n// 加载占位默认的退出动画\nval DEFAULT_PLACEHOLDER_EXIT_TRANSITION = fadeOut(tween(200))\n\n// 默认的加载占位\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nval DEFAULT_PREVIEWER_PLACEHOLDER_CONTENT = @Composable {\n    Box(\n        modifier = Modifier.fillMaxSize(),\n        contentAlignment = Alignment.Center\n    ) {\n        LoadingIndicator()\n        //CircularProgressIndicator(color = Color.White.copy(0.2F))\n    }\n}\n\n// 加载时的占位内容\nclass PreviewerPlaceholder(\n    // 进入动画\n    var enterTransition: EnterTransition = DEFAULT_PLACEHOLDER_ENTER_TRANSITION,\n    // 退出动画\n    var exitTransition: ExitTransition = DEFAULT_PLACEHOLDER_EXIT_TRANSITION,\n    // 占位的内容\n    var content: @Composable () -> Unit = DEFAULT_PREVIEWER_PLACEHOLDER_CONTENT,\n)\n\n/**\n * 预览图层对象\n */\nclass PreviewerLayerScope(\n    // 包裹viewer的容器图层\n    var viewerContainer: @Composable (\n        page: Int, viewerState: ImageViewerState, viewer: @Composable () -> Unit\n    ) -> Unit = { _, _, viewer -> viewer() },\n    // 背景图层\n    var background: @Composable ((page: Int) -> Unit) = { _ -> DefaultPreviewerBackground() },\n    // 前景图层\n    var foreground: @Composable ((page: Int) -> Unit) = { _ -> },\n    // 加载时的占位内容\n    var placeholder: PreviewerPlaceholder = PreviewerPlaceholder()\n)\n\n/**\n * 图片预览组件\n */\n@Composable\nfun ImagePreviewer(\n    // 编辑参数\n    modifier: Modifier = Modifier,\n    // 状态对象\n    state: ImagePreviewerState,\n    // 图片加载器\n    imageLoader: @Composable (Int) -> Any?,\n    // 图片间的间隔\n    itemSpacing: Dp = DEFAULT_ITEM_SPACE,\n    // 进入动画\n    enter: EnterTransition = DEFAULT_PREVIEWER_ENTER_TRANSITION,\n    // 退出动画\n    exit: ExitTransition = DEFAULT_PREVIEWER_EXIT_TRANSITION,\n    // 检测手势\n    detectGesture: GalleryGestureScope.() -> Unit = {},\n    // 自定义previewer的各个图层\n    previewerLayer: PreviewerLayerScope.() -> Unit = {},\n) {\n    state.apply {\n        // 图层相关\n        val layerScope = remember { PreviewerLayerScope() }\n        previewerLayer.invoke(layerScope)\n        LaunchedEffect(\n            key1 = animateContainerVisibleState,\n            key2 = animateContainerVisibleState.currentState\n        ) {\n            onAnimateContainerStateChanged()\n        }\n        AnimatedVisibility(\n            modifier = Modifier.fillMaxSize(),\n            visibleState = animateContainerVisibleState,\n            enter = enterTransition ?: enter,\n            exit = exitTransition ?: exit,\n        ) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .pointerInput(getKey) {\n                        verticalDrag(this)\n                    }\n            ) {\n                @Composable\n                fun UIContainer(content: @Composable () -> Unit) {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .alpha(uiAlpha.value)\n                    ) {\n                        content()\n                    }\n                }\n                ImageGallery(\n                    modifier = modifier.fillMaxSize(),\n                    state = galleryState,\n                    imageLoader = imageLoader,\n                    itemSpacing = itemSpacing,\n                    detectGesture = detectGesture,\n                    galleryLayer = {\n                        this.viewerContainer = { page, viewerState, viewer ->\n                            layerScope.viewerContainer(page, viewerState) {\n                                val viewerContainerState = rememberViewerContainerState(\n                                    viewerState = viewerState,\n                                    animationSpec = defaultAnimationSpec\n                                )\n                                LaunchedEffect(key1 = currentPage) {\n                                    if (currentPage == page) {\n                                        state.viewerContainerState = viewerContainerState\n                                    }\n                                }\n                                ImageViewerContainer(\n                                    modifier = Modifier.alpha(viewerAlpha.value),\n                                    containerState = viewerContainerState,\n                                    placeholder = layerScope.placeholder,\n                                    viewer = viewer,\n                                )\n                            }\n                        }\n                        this.background = {\n                            UIContainer {\n                                layerScope.background(it)\n                            }\n                        }\n                        this.foreground = {\n                            UIContainer {\n                                layerScope.foreground(it)\n                            }\n                        }\n                    },\n                )\n                if (!visible)\n                    Box(\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .pointerInput(Unit) { detectTapGestures { } }) { }\n            }\n        }\n        ticket.Next()\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/ImageTransform.kt",
    "content": "package com.origeek.imageViewer.previewer\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.geometry.isSpecified\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.TransformOrigin\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.layout.positionInRoot\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.IntSize\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.takeWhile\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\n\n/**\n * @program: ImageViewer\n *\n * @description:\n *\n * @author: JVZIYAOYAO\n *\n * @create: 2022-09-22 10:13\n **/\n\n// 用于操作transformItemStateMap的锁对象\ninternal val imageTransformMutex = Mutex()\n\n// 用于缓存界面上的transformItemState\ninternal val transformItemStateMap = mutableStateMapOf<Any, TransformItemState>()\n\n@Composable\nfun TransformImageView(\n    modifier: Modifier = Modifier,\n    painter: Painter,\n    key: Any,\n    itemState: TransformItemState = rememberTransformItemState(),\n    previewerState: ImagePreviewerState,\n) {\n    TransformImageView(\n        modifier = modifier,\n        key = key,\n        itemState = itemState,\n        contentState = previewerState.transformState,\n    ) { itemKey ->\n        key(itemKey) {\n            LaunchedEffect(key1 = painter.intrinsicSize) {\n                if (painter.intrinsicSize.isSpecified) {\n                    itemState.intrinsicSize = painter.intrinsicSize\n                }\n            }\n            Image(\n                modifier = Modifier.fillMaxSize(),\n                painter = painter,\n                contentDescription = null,\n                contentScale = ContentScale.Crop,\n            )\n        }\n    }\n}\n\n@Composable\nfun TransformImageView(\n    modifier: Modifier = Modifier,\n    bitmap: ImageBitmap,\n    key: Any,\n    itemState: TransformItemState = rememberTransformItemState(),\n    previewerState: ImagePreviewerState,\n) {\n    TransformImageView(\n        modifier = modifier,\n        key = key,\n        itemState = itemState,\n        previewerState = previewerState,\n    ) {\n        itemState.intrinsicSize = Size(\n            bitmap.width.toFloat(),\n            bitmap.height.toFloat()\n        )\n        Image(\n            modifier = Modifier.fillMaxSize(),\n            bitmap = bitmap,\n            contentDescription = null,\n            contentScale = ContentScale.Crop,\n        )\n    }\n}\n\n@Composable\nfun TransformImageView(\n    modifier: Modifier = Modifier,\n    imageVector: ImageVector,\n    key: Any,\n    itemState: TransformItemState = rememberTransformItemState(),\n    previewerState: ImagePreviewerState,\n) {\n    TransformImageView(\n        modifier = modifier,\n        key = key,\n        itemState = itemState,\n        previewerState = previewerState,\n    ) {\n        LocalDensity.current.run {\n            itemState.intrinsicSize = Size(\n                imageVector.defaultWidth.toPx(),\n                imageVector.defaultHeight.toPx(),\n            )\n        }\n        Image(\n            modifier = Modifier.fillMaxSize(),\n            imageVector = imageVector,\n            contentDescription = null,\n            contentScale = ContentScale.Crop,\n        )\n    }\n}\n\n@Composable\nfun TransformImageView(\n    modifier: Modifier = Modifier,\n    key: Any,\n    itemState: TransformItemState = rememberTransformItemState(),\n    previewerState: ImagePreviewerState,\n    content: @Composable (Any) -> Unit,\n) = TransformImageView(modifier, key, itemState, previewerState.transformState, content)\n\n@Composable\nfun TransformImageView(\n    modifier: Modifier = Modifier,\n    key: Any,\n    itemState: TransformItemState = rememberTransformItemState(),\n    contentState: TransformContentState? = rememberTransformContentState(),\n    content: @Composable (Any) -> Unit,\n) {\n    TransformItemView(\n        modifier = modifier,\n        key = key,\n        itemState = itemState,\n        contentState = contentState,\n    ) {\n        content(key)\n    }\n}\n\n@Composable\nfun TransformItemView(\n    modifier: Modifier = Modifier,\n    key: Any,\n    itemState: TransformItemState = rememberTransformItemState(),\n    contentState: TransformContentState?,\n    content: @Composable (Any) -> Unit,\n) {\n    val scope = rememberCoroutineScope()\n    itemState.key = key\n    itemState.blockCompose = content\n    DisposableEffect(key) {\n        // 这个composable加载时添加到map\n        scope.launch {\n            itemState.addItem()\n        }\n        onDispose {\n            // composable退出时从map移除\n            itemState.removeItem()\n        }\n    }\n    Box(\n        modifier = modifier\n            .onGloballyPositioned {\n                itemState.onPositionChange(\n                    position = it.positionInRoot(),\n                    size = it.size,\n                )\n            }\n            .fillMaxSize()\n    ) {\n        if (\n            contentState?.itemState != itemState || !contentState.onAction\n        ) {\n            itemState.blockCompose(key)\n        }\n    }\n}\n\n@Composable\nfun TransformContentView(\n    transformContentState: TransformContentState = rememberTransformContentState(),\n) {\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .onGloballyPositioned {\n                transformContentState.containerSize = it.size\n                transformContentState.containerOffset = it.positionInRoot()\n            },\n    ) {\n        if (\n            transformContentState.srcCompose != null\n            && transformContentState.onAction\n        ) {\n            Box(\n                modifier = Modifier\n                    .offset(\n                        x = LocalDensity.current.run { (transformContentState.offsetX.value).toDp() },\n                        y = LocalDensity.current.run { (transformContentState.offsetY.value).toDp() },\n                    )\n                    .size(\n                        width = LocalDensity.current.run { transformContentState.displayWidth.value.toDp() },\n                        height = LocalDensity.current.run { transformContentState.displayHeight.value.toDp() },\n                    )\n                    .graphicsLayer {\n                        transformOrigin = TransformOrigin(0F, 0F)\n                        scaleX = transformContentState.graphicScaleX.value\n                        scaleY = transformContentState.graphicScaleY.value\n                    },\n            ) {\n                transformContentState.srcCompose!!(transformContentState.itemState?.key ?: Unit)\n            }\n        }\n    }\n}\n\nclass TransformContentState(\n    // 协程作用域\n    var scope: CoroutineScope = MainScope(),\n    // 默认动画窗格\n    var defaultAnimationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC\n) {\n\n    var itemState: TransformItemState? by mutableStateOf(null)\n\n    val intrinsicSize: Size\n        get() = itemState?.intrinsicSize ?: Size.Zero\n\n    val intrinsicRatio: Float\n        get() {\n            if (intrinsicSize.height == 0F) return 1F\n            return intrinsicSize.width.div(intrinsicSize.height)\n        }\n\n    val srcPosition: Offset\n        get() {\n            val offset = itemState?.blockPosition ?: Offset.Zero\n            return offset.copy(x = offset.x - containerOffset.x, y = offset.y - containerOffset.y)\n        }\n\n    val srcSize: IntSize\n        get() = itemState?.blockSize ?: IntSize.Zero\n\n    val srcCompose: (@Composable (Any) -> Unit)?\n        get() = itemState?.blockCompose\n\n    var onAction by mutableStateOf(false)\n\n    var onActionTarget by mutableStateOf<Boolean?>(null)\n\n    var displayWidth = Animatable(0F)\n\n    var displayHeight = Animatable(0F)\n\n    var graphicScaleX = Animatable(1F)\n\n    var graphicScaleY = Animatable(1F)\n\n    var offsetX = Animatable(0F)\n\n    var offsetY = Animatable(0F)\n\n    var containerOffset by mutableStateOf(Offset.Zero)\n\n    private var containerSizeState = mutableStateOf(IntSize.Zero)\n\n    var containerSize: IntSize\n        get() = containerSizeState.value\n        set(value) {\n            containerSizeState.value = value\n            if (value.width != 0 && value.height != 0) {\n                scope.launch {\n                    specifierSizeFlow.emit(true)\n                }\n            }\n        }\n\n    var specifierSizeFlow = MutableStateFlow(false)\n\n    val containerRatio: Float\n        get() {\n            if (containerSize.height == 0) return 1F\n            return containerSize.width.toFloat().div(containerSize.height)\n        }\n\n    val widthFixed: Boolean\n        get() = intrinsicRatio > containerRatio\n\n    val fitSize: Size\n        get() {\n            return if (intrinsicRatio > containerRatio) {\n                // 宽度一致\n                val uW = containerSize.width\n                val uH = uW / intrinsicRatio\n                Size(uW.toFloat(), uH)\n            } else {\n                // 高度一致\n                val uH = containerSize.height\n                val uW = uH * intrinsicRatio\n                Size(uW, uH.toFloat())\n            }\n        }\n\n    val fitOffsetX: Float\n        get() {\n            return (containerSize.width - fitSize.width).div(2)\n        }\n\n    val fitOffsetY: Float\n        get() {\n            return (containerSize.height - fitSize.height).div(2)\n        }\n\n    val fitScale: Float\n        get() {\n            return fitSize.width.div(displayRatioSize.width)\n        }\n\n    val displayRatioSize: Size\n        get() {\n            return Size(width = srcSize.width.toFloat(), height = srcSize.width.div(intrinsicRatio))\n        }\n\n    val realSize: Size\n        get() {\n            return Size(\n                width = displayWidth.value * graphicScaleX.value,\n                height = displayHeight.value * graphicScaleY.value,\n            )\n        }\n\n    suspend fun awaitContainerSizeSpecifier() {\n        specifierSizeFlow.takeWhile { !it }.collect {}\n    }\n\n    fun findTransformItem(key: Any) = transformItemStateMap[key]\n\n    fun clearTransformItems() = transformItemStateMap.clear()\n\n    fun setEnterState() {\n        onAction = true\n        onActionTarget = null\n    }\n\n    fun setExitState() {\n        onAction = false\n        onActionTarget = null\n    }\n\n    suspend fun notifyEnterChanged() {\n        scope.launch {\n            listOf(\n                scope.async {\n                    displayWidth.snapTo(displayRatioSize.width)\n                },\n                scope.async {\n                    displayHeight.snapTo(displayRatioSize.height)\n                },\n                scope.async {\n                    graphicScaleX.snapTo(fitScale)\n                },\n                scope.async {\n                    graphicScaleY.snapTo(fitScale)\n                },\n                scope.async {\n                    offsetX.snapTo(fitOffsetX)\n                },\n                scope.async {\n                    offsetY.snapTo(fitOffsetY)\n                },\n            ).awaitAll()\n        }\n    }\n\n    suspend fun exitTransform(\n        animationSpec: AnimationSpec<Float>? = null\n    ) = suspendCoroutine<Unit> { c ->\n        val currentAnimateSpec = animationSpec ?: defaultAnimationSpec\n        scope.launch {\n            listOf(\n                scope.async {\n                    displayWidth.animateTo(srcSize.width.toFloat(), currentAnimateSpec)\n                },\n                scope.async {\n                    displayHeight.animateTo(srcSize.height.toFloat(), currentAnimateSpec)\n                },\n                scope.async {\n                    graphicScaleX.animateTo(1F, currentAnimateSpec)\n                },\n                scope.async {\n                    graphicScaleY.animateTo(1F, currentAnimateSpec)\n                },\n                scope.async {\n                    offsetX.animateTo(srcPosition.x, currentAnimateSpec)\n                },\n                scope.async {\n                    offsetY.animateTo(srcPosition.y, currentAnimateSpec)\n                },\n            ).awaitAll()\n            onAction = false\n            onActionTarget = null\n            c.resume(Unit)\n        }\n    }\n\n    suspend fun enterTransform(\n        itemState: TransformItemState,\n        animationSpec: AnimationSpec<Float>? = null\n    ) = suspendCoroutine<Unit> { c ->\n        val currentAnimationSpec = animationSpec ?: defaultAnimationSpec\n        this.itemState = itemState\n\n        displayWidth = Animatable(srcSize.width.toFloat())\n        displayHeight = Animatable(srcSize.height.toFloat())\n        graphicScaleX = Animatable(1F)\n        graphicScaleY = Animatable(1F)\n\n        offsetX = Animatable(srcPosition.x)\n        offsetY = Animatable(srcPosition.y)\n\n        onActionTarget = true\n        onAction = true\n\n        scope.launch {\n            reset(currentAnimationSpec)\n            c.resume(Unit)\n            onActionTarget = null\n        }\n    }\n\n    suspend fun reset(animationSpec: AnimationSpec<Float>? = null) {\n        val currentAnimationSpec = animationSpec ?: defaultAnimationSpec\n        listOf(\n            scope.async {\n                displayWidth.animateTo(displayRatioSize.width, currentAnimationSpec)\n            },\n            scope.async {\n                displayHeight.animateTo(displayRatioSize.height, currentAnimationSpec)\n            },\n            scope.async {\n                graphicScaleX.animateTo(fitScale, currentAnimationSpec)\n            },\n            scope.async {\n                graphicScaleY.animateTo(fitScale, currentAnimationSpec)\n            },\n            scope.async {\n                offsetX.animateTo(fitOffsetX, currentAnimationSpec)\n            },\n            scope.async {\n                offsetY.animateTo(fitOffsetY, currentAnimationSpec)\n            },\n        ).awaitAll()\n    }\n\n    companion object {\n        val Saver: Saver<TransformContentState, *> = listSaver(\n            save = {\n                listOf<Any>(\n                    it.onAction,\n                )\n            },\n            restore = {\n                val transformContentState = TransformContentState()\n                transformContentState.onAction = it[0] as Boolean\n                transformContentState\n            }\n        )\n    }\n\n}\n\n@Composable\nfun rememberTransformContentState(\n    scope: CoroutineScope = rememberCoroutineScope(),\n    animationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC\n): TransformContentState {\n    val transformContentState = rememberSaveable(saver = TransformContentState.Saver) {\n        TransformContentState()\n    }\n    transformContentState.scope = scope\n    transformContentState.defaultAnimationSpec = animationSpec\n    return transformContentState\n}\n\nclass TransformItemState(\n    var key: Any = Unit,\n    var blockCompose: (@Composable (Any) -> Unit) = {},\n    var scope: CoroutineScope,\n    var blockPosition: Offset = Offset.Zero,\n    var blockSize: IntSize = IntSize.Zero,\n    var intrinsicSize: Size? = null,\n    var checkInBound: (TransformItemState.() -> Boolean)? = null,\n) {\n\n    private fun checkItemInMap() {\n        if (checkInBound == null) return\n        if (checkInBound!!.invoke(this)) {\n            addItem()\n        } else {\n            removeItem()\n        }\n    }\n\n    /**\n     * 位置和大小发生变化时\n     * @param position Offset\n     * @param size IntSize\n     */\n    internal fun onPositionChange(position: Offset, size: IntSize) {\n        blockPosition = position\n        blockSize = size\n        scope.launch {\n            checkItemInMap()\n        }\n    }\n\n    /**\n     * 判断item是否在所需范围内，返回true，则添加该item到map，返回false则移除\n     * @param checkInBound Function0<Boolean>\n     */\n    fun checkIfInBound(checkInBound: () -> Boolean) {\n        if (checkInBound()) {\n            addItem()\n        } else {\n            removeItem()\n        }\n    }\n\n    /**\n     * 添加item到map上\n     * @param key Any?\n     */\n    fun addItem(key: Any? = null) {\n        val currentKey = key ?: this.key ?: return\n        if (checkInBound != null) return\n        synchronized(imageTransformMutex) {\n            transformItemStateMap[currentKey] = this\n        }\n    }\n\n    /**\n     * 从map上移除item\n     * @param key Any?\n     */\n    fun removeItem(key: Any? = null) {\n        synchronized(imageTransformMutex) {\n            val currentKey = key ?: this.key ?: return\n            if (checkInBound != null) return\n            transformItemStateMap.remove(currentKey)\n        }\n    }\n}\n\n@Composable\nfun rememberTransformItemState(\n    scope: CoroutineScope = rememberCoroutineScope(),\n    checkInBound: (TransformItemState.() -> Boolean)? = null,\n): TransformItemState {\n    return remember { TransformItemState(scope = scope, checkInBound = checkInBound) }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/ImageViewerContainer.kt",
    "content": "package com.origeek.imageViewer.previewer\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.mapSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.unit.IntSize\nimport com.origeek.imageViewer.viewer.ImageViewerState\nimport com.origeek.imageViewer.viewer.rememberViewerState\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.flow.collect\nimport kotlinx.coroutines.flow.takeWhile\nimport kotlinx.coroutines.withContext\n\n/**\n * @program: ImageViewer\n *\n * @description:\n *\n * @author: JVZIYAOYAO\n *\n * @create: 2022-10-17 14:45\n **/\n\ninternal class ViewerContainerState(\n    // 协程作用域\n    var scope: CoroutineScope = MainScope(),\n    // 转换图层的状态\n    var transformState: TransformContentState = TransformContentState(),\n    // viewer的状态\n    var imageViewerState: ImageViewerState = ImageViewerState(),\n    // 默认动画窗格\n    var defaultAnimationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC\n) {\n\n    /**\n     *   +-------------------+\n     *         INTERNAL\n     *   +-------------------+\n     */\n\n    // 转换图层transformContent透明度\n    internal var transformContentAlpha = Animatable(0F)\n\n    // viewer容器的透明度\n    internal var viewerContainerAlpha = Animatable(1F)\n\n    // 是否允许界面显示loading\n    internal var allowLoading by mutableStateOf(true)\n\n    // 打开图片后到加载成功过程的协程任务\n    internal var openTransformJob: Deferred<Unit>? = null\n\n    /**\n     * 取消打开动作\n     */\n    internal fun cancelOpenTransform() {\n        openTransformJob?.cancel()\n        openTransformJob = null\n    }\n\n    /**\n     * 等待挂载成功\n     */\n    internal suspend fun awaitOpenTransform() {\n        // 这里需要等待viewer挂载，显示loading界面\n        openTransformJob = scope.async {\n            // 等待viewer加载\n            awaitViewerLoading()\n            // viewer加载成功后显示viewer\n            transformSnapToViewer(true)\n        }\n        openTransformJob?.await()\n        openTransformJob = null\n    }\n\n    /**\n     * 等待viewer挂载成功\n     */\n    internal suspend fun awaitViewerLoading() {\n        imageViewerState.mountedFlow.apply {\n            withContext(Dispatchers.Default) {\n                takeWhile { !it }.collect()\n            }\n        }\n    }\n\n    /**\n     * 转换图层转viewer图层，true显示viewer，false显示转换图层\n     * @param isViewer Boolean\n     */\n    internal suspend fun transformSnapToViewer(isViewer: Boolean) {\n        if (isViewer) {\n            transformContentAlpha.snapTo(0F)\n            viewerContainerAlpha.snapTo(1F)\n        } else {\n            transformContentAlpha.snapTo(1F)\n            viewerContainerAlpha.snapTo(0F)\n        }\n    }\n\n    /**\n     * 将viewer容器的位置大小复制给transformContent\n     */\n    internal suspend fun copyViewerContainerStateToTransformState() {\n        transformState.apply {\n            val targetScale = scale.value * fitScale\n            graphicScaleX.snapTo(targetScale)\n            graphicScaleY.snapTo(targetScale)\n            val centerOffsetY = (containerSize.height - realSize.height).div(2)\n            val centerOffsetX = (containerSize.width - realSize.width).div(2)\n            offsetY.snapTo(centerOffsetY + this@ViewerContainerState.offsetY.value)\n            offsetX.snapTo(centerOffsetX + this@ViewerContainerState.offsetX.value)\n        }\n    }\n\n    /**\n     * 将viewer的位置大小等信息复制给transformContent\n     * @param itemState TransformItemState\n     */\n    internal suspend fun copyViewerPosToContent(itemState: TransformItemState) {\n        transformState.apply {\n            // 更新itemState，确保itemState一致\n            this@apply.itemState = itemState\n            // 确保viewer的容器大小与transform的容器大小一致\n            containerSize = imageViewerState.containerSize\n            val scale = imageViewerState.scale\n            val offsetX = imageViewerState.offsetX\n            val offsetY = imageViewerState.offsetY\n            // 计算transform的实际大小\n            val rw = fitSize.width * scale.value\n            val rh = fitSize.height * scale.value\n            // 计算目标平移量\n            val goOffsetX =\n                (containerSize.width - rw).div(2) + offsetX.value\n            val goOffsetY =\n                (containerSize.height - rh).div(2) + offsetY.value\n            // 计算缩放率\n            val fixScale = fitScale * scale.value\n\n            // 更新值\n            graphicScaleX.snapTo(fixScale)\n            graphicScaleY.snapTo(fixScale)\n            displayWidth.snapTo(displayRatioSize.width)\n            displayHeight.snapTo(displayRatioSize.height)\n            this@apply.offsetX.snapTo(goOffsetX)\n            this@apply.offsetY.snapTo(goOffsetY)\n        }\n    }\n\n    // 容器大小\n    var containerSize: IntSize by mutableStateOf(IntSize.Zero)\n\n    // 容器的偏移量X\n    var offsetX = Animatable(0F)\n\n    // 容器的偏移量Y\n    var offsetY = Animatable(0F)\n\n    // 容器缩放\n    var scale = Animatable(1F)\n\n    /**\n     * 重置回原来的状态\n     * @param animationSpec AnimationSpec<Float>\n     */\n    suspend fun reset(animationSpec: AnimationSpec<Float> = defaultAnimationSpec) {\n        scope.apply {\n            listOf(\n                async {\n                    offsetX.animateTo(0F, animationSpec)\n                },\n                async {\n                    offsetY.animateTo(0F, animationSpec)\n                },\n                async {\n                    scale.animateTo(1F, animationSpec)\n                },\n            ).awaitAll()\n        }\n    }\n\n    /**\n     * 立刻重置\n     */\n    suspend fun resetImmediately() {\n        offsetX.snapTo(0F)\n        offsetY.snapTo(0F)\n        scale.snapTo(1F)\n    }\n\n    companion object {\n        val Saver: Saver<ViewerContainerState, *> = mapSaver(\n            save = {\n                mapOf<String, Any>(\n                    it::offsetX.name to it.offsetX.value,\n                    it::offsetY.name to it.offsetY.value,\n                    it::scale.name to it.scale.value,\n                )\n            },\n            restore = {\n                val viewerContainerState = ViewerContainerState()\n                viewerContainerState.offsetX =\n                    Animatable(it[viewerContainerState::offsetX.name] as Float)\n                viewerContainerState.offsetY =\n                    Animatable(it[viewerContainerState::offsetY.name] as Float)\n                viewerContainerState.scale =\n                    Animatable(it[viewerContainerState::scale.name] as Float)\n                viewerContainerState\n            }\n        )\n    }\n}\n\n/**\n * 记录Viewer容器的状态\n * @return ViewerContainerState\n */\n@Composable\ninternal fun rememberViewerContainerState(\n    // 协程作用域\n    scope: CoroutineScope = rememberCoroutineScope(),\n    // viewer状态\n    viewerState: ImageViewerState = rememberViewerState(),\n    // 转换content的state\n    transformContentState: TransformContentState = rememberTransformContentState(),\n    // 动画窗格\n    animationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC,\n): ViewerContainerState {\n    val viewerContainerState = rememberSaveable(saver = ViewerContainerState.Saver) {\n        ViewerContainerState()\n    }\n    viewerContainerState.scope = scope\n    viewerContainerState.imageViewerState = viewerState\n    viewerContainerState.transformState = transformContentState\n    viewerContainerState.defaultAnimationSpec = animationSpec\n    return viewerContainerState\n}\n\n/**\n * Viewer容器\n */\n@Composable\ninternal fun ImageViewerContainer(\n    // 修改对象\n    modifier: Modifier = Modifier,\n    // 容器状态\n    containerState: ViewerContainerState,\n    // 未加载成功时的占位\n    placeholder: PreviewerPlaceholder = PreviewerPlaceholder(),\n    // viewer主体\n    viewer: @Composable () -> Unit,\n) {\n    containerState.apply {\n        Box(\n            modifier = modifier\n                .fillMaxSize()\n                .onGloballyPositioned {\n                    containerSize = it.size\n                }\n                .graphicsLayer {\n                    scaleX = scale.value\n                    scaleY = scale.value\n                    translationX = offsetX.value\n                    translationY = offsetY.value\n                }\n        ) {\n            // 支持转换效果的图层\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .alpha(transformContentAlpha.value)\n            ) {\n                TransformContentView(transformState)\n            }\n            // viewer图层\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .alpha(viewerContainerAlpha.value)\n            ) {\n                viewer()\n            }\n            // 判断viewer是否加载成功\n            val viewerMounted by imageViewerState.mountedFlow.collectAsState(\n                initial = false\n            )\n            if (allowLoading) AnimatedVisibility(\n                visible = !viewerMounted,\n                enter = placeholder.enterTransition,\n                exit = placeholder.exitTransition,\n            ) {\n                placeholder.content()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/PreviewerPagerState.kt",
    "content": "package com.origeek.imageViewer.previewer\n\nimport androidx.annotation.FloatRange\nimport androidx.annotation.IntRange\nimport com.origeek.imageViewer.gallery.ImageGalleryState\n\n/**\n * @program: ImageViewer\n *\n * @description:\n *\n * @author: JVZIYAOYAO\n *\n * @create: 2022-10-17 14:41\n **/\n\nopen class PreviewerPagerState(\n    val galleryState: ImageGalleryState,\n) {\n\n    /**\n     * 当前页码\n     */\n    val currentPage: Int\n        get() = galleryState.currentPage\n\n    /**\n     * 目标页码\n     */\n    val targetPage: Int\n        get() = galleryState.targetPage\n\n    /**\n     * 滚动到指定页面\n     * @param page Int\n     * @param pageOffset Float\n     */\n    suspend fun scrollToPage(\n        @IntRange(from = 0) page: Int,\n        @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0F,\n    ) = galleryState.scrollToPage(page, pageOffset)\n\n    /**\n     * 带动画滚动到指定页面\n     * @param page Int\n     * @param pageOffset Float\n     */\n    suspend fun animateScrollToPage(\n        @IntRange(from = 0) page: Int,\n        @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0F,\n    ) = galleryState.animateScrollToPage(page, pageOffset)\n\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/PreviewerTransformState.kt",
    "content": "package com.origeek.imageViewer.previewer\n\nimport androidx.compose.animation.EnterTransition\nimport androidx.compose.animation.ExitTransition\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.MutableTransitionState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport com.origeek.imageViewer.gallery.ImageGalleryState\nimport com.origeek.imageViewer.util.Ticket\nimport com.origeek.imageViewer.viewer.ImageViewerState\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\n\n/**\n * @program: ImageViewer\n *\n * @description:\n *\n * @author: JVZIYAOYAO\n *\n * @create: 2022-10-17 14:41\n **/\n\nopen class PreviewerTransformState(\n    // 协程作用域\n    var scope: CoroutineScope = MainScope(),\n    // 默认动画窗格\n    var defaultAnimationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC,\n    // 预览状态\n    galleryState: ImageGalleryState,\n) : PreviewerPagerState(\n    galleryState = galleryState,\n) {\n\n    /**\n     *   +-------------------+\n     *          PRIVATE\n     *   +-------------------+\n     */\n\n    // 锁对象\n    private var mutex = Mutex()\n\n    // 打开回调，最外层animateVisible修改时调用\n    private var openCallback: (() -> Unit)? = null\n\n    // 关闭回调，最外层animateVisible修改时调用\n    private var closeCallback: (() -> Unit)? = null\n\n    // 是否显示viewer容器的标识\n    private val viewerContainerVisible: Boolean\n        get() = viewerContainerState?.viewerContainerAlpha?.value == 1F\n\n    /**\n     * 更新当前的标记状态\n     * @param animating Boolean\n     * @param visible Boolean\n     * @param visibleTarget Boolean?\n     */\n    private suspend fun updateState(animating: Boolean, visible: Boolean, visibleTarget: Boolean?) {\n        mutex.withLock {\n            this.animating = animating\n            this.visible = visible\n            this.visibleTarget = visibleTarget\n        }\n    }\n\n    /**\n     *   +-------------------+\n     *         INTERNAL\n     *   +-------------------+\n     */\n\n    // 等待界面刷新的ticket\n    internal val ticket = Ticket()\n\n    // 最外侧animateVisibleState\n    internal var animateContainerVisibleState by mutableStateOf(MutableTransitionState(false))\n\n    // UI透明度\n    internal var uiAlpha = Animatable(0F)\n\n    // viewer透明度\n    internal var viewerAlpha = Animatable(1F)\n\n    // 从外部传入viewer容器\n    internal var viewerContainerState by mutableStateOf<ViewerContainerState?>(null)\n\n    // 从外部提供transformContentState\n    internal val transformState: TransformContentState?\n        get() = viewerContainerState?.transformState\n\n    // 进入转换动画\n    internal var enterTransition: EnterTransition? = null\n\n    // 离开的转换动画\n    internal var exitTransition: ExitTransition? = null\n\n    // 判断是否允许transform结束\n    internal val canTransformOut: Boolean\n        get() = (viewerContainerState?.openTransformJob != null) || (imageViewerState?.mountedFlow?.value == true)\n\n    // 标记打开动作，执行开始\n    internal suspend fun stateOpenStart() =\n        updateState(animating = true, visible = false, visibleTarget = true)\n\n    // 标记打开动作，执行结束\n    internal suspend fun stateOpenEnd() =\n        updateState(animating = false, visible = true, visibleTarget = null)\n\n    // 标记关闭动作，执行开始\n    internal suspend fun stateCloseStart() =\n        updateState(animating = true, visible = true, visibleTarget = false)\n\n    // 标记关闭动作，执行结束\n    internal suspend fun stateCloseEnd() =\n        updateState(animating = false, visible = false, visibleTarget = null)\n\n    /**\n     * 转换图层转viewer图层，true显示viewer，false显示转换图层\n     * @param isViewer Boolean\n     */\n    internal suspend fun transformSnapToViewer(isViewer: Boolean) {\n        if (isViewer && visibleTarget == false) return\n        viewerContainerState?.transformSnapToViewer(isViewer)\n    }\n\n    /**\n     * animateVisable执行完成后调用回调方法\n     */\n    internal fun onAnimateContainerStateChanged() {\n        if (animateContainerVisibleState.currentState) {\n            openCallback?.invoke()\n            transformState?.setEnterState()\n        } else {\n            closeCallback?.invoke()\n        }\n    }\n\n    /**\n     *   +-------------------+\n     *          PUBLIC\n     *   +-------------------+\n     */\n\n    // 是否正在进行动画\n    var animating by mutableStateOf(false)\n        internal set\n\n    // 是否可见\n    var visible by mutableStateOf(false)\n        internal set\n\n    // 是否可见的目标值\n    var visibleTarget by mutableStateOf<Boolean?>(null)\n        internal set\n\n    // 是否允许执行open操作\n    val canOpen: Boolean\n        get() = !visible && visibleTarget == null && !animating\n\n    // 是否允许执行close操作\n    val canClose: Boolean\n        get() = visible && visibleTarget == null && !animating\n\n    // imageViewer状态对象\n    val imageViewerState: ImageViewerState?\n        get() = viewerContainerState?.imageViewerState\n\n    /**\n     * 根据页面获取当前页码所属的key\n     */\n    var getKey: ((Int) -> Any)? = null\n\n    // 查找key关联的transformItem\n    fun findTransformItem(key: Any): TransformItemState? {\n        return transformItemStateMap[key]\n    }\n\n    // 根据index查询key\n    fun findTransformItemByIndex(index: Int): TransformItemState? {\n        val key = getKey?.invoke(index) ?: return null\n        return findTransformItem(key)\n    }\n\n    // 清除全部transformItems\n    fun clearTransformItems() = transformItemStateMap.clear()\n\n    /**\n     * 打开previewer\n     * @param index Int\n     * @param itemState TransformItemState?\n     * @param enterTransition EnterTransition?\n     */\n    suspend fun open(\n        index: Int = 0,\n        itemState: TransformItemState? = null,\n        enterTransition: EnterTransition? = null\n    ) =\n        suspendCoroutine<Unit> { c ->\n            // 设置当前转换动画\n            this.enterTransition = enterTransition\n            // 设置转换回调\n            openCallback = {\n                c.resume(Unit)\n                // 清除转换回调\n                openCallback = null\n                // 清除转换动画\n                this.enterTransition = null\n                // 标记结束\n                scope.launch {\n                    stateOpenEnd()\n                }\n            }\n            scope.launch {\n                // 标记开始\n                stateOpenStart()\n                // 开启UI\n                uiAlpha.snapTo(1F)\n                // container动画立即设置为关闭\n                animateContainerVisibleState = MutableTransitionState(false)\n                // 开启container\n                animateContainerVisibleState.targetState = true\n                // 跳转到index\n                galleryState.scrollToPage(index)\n                // 等待下一帧之后viewerContainerState才会刷新出来\n                ticket.awaitNextTicket()\n                // 允许显示loading\n                viewerContainerState?.allowLoading = true\n                // 开启viewer\n                viewerContainerState?.viewerContainerAlpha?.snapTo(1F)\n                // 如果输入itemState，则用itemState做为背景\n                if (itemState != null) {\n                    scope.launch {\n                        viewerContainerState?.transformContentAlpha?.snapTo(1F)\n                        transformState?.awaitContainerSizeSpecifier()\n                        transformState?.enterTransform(itemState, tween(0))\n                    }\n                }\n                // 这里需要等待viewer挂载，显示loading界面\n                viewerContainerState?.awaitOpenTransform()\n            }\n        }\n\n    /**\n     * 关闭previewer\n     * @param exitTransition ExitTransition?\n     */\n    suspend fun close(exitTransition: ExitTransition? = null) = suspendCoroutine<Unit> { c ->\n        // 设置当前退出动画\n        this.exitTransition = exitTransition\n        // 设置退出结束的回调方法\n        closeCallback = {\n            c.resume(Unit)\n            // 将回调设置为空\n            closeCallback = null\n            // 将退出动画设置为空\n            this.exitTransition = null\n            // 标记结束\n            scope.launch {\n                stateCloseEnd()\n            }\n        }\n        scope.launch {\n            // 标记开始\n            stateCloseStart()\n            // 关闭正在进行的开启操作\n            viewerContainerState?.cancelOpenTransform()\n            // 这里创建一个全新的state是为了让exitTransition的设置得到响应\n            animateContainerVisibleState = MutableTransitionState(true)\n            // 开启container关闭动画\n            animateContainerVisibleState.targetState = false\n            // 等待下一帧\n            ticket.awaitNextTicket()\n            // transformState标记退出\n            transformState?.setExitState()\n        }\n    }\n\n    /**\n     * 打开previewer，带转换效果\n     * @param index Int\n     * @param itemState TransformItemState\n     * @param animationSpec AnimationSpec<Float>?\n     */\n    suspend fun openTransform(\n        index: Int,\n        itemState: TransformItemState? = findTransformItemByIndex(index),\n        animationSpec: AnimationSpec<Float> = defaultAnimationSpec\n    ) {\n        // 如果itemState为空，改用open的方式打开\n        if (itemState == null) {\n            open(index)\n            return\n        }\n        // 动画开始\n        stateOpenStart()\n        // 关闭UI\n        uiAlpha.snapTo(0F)\n        // 关闭viewer\n        viewerAlpha.snapTo(0F)\n        // 设置新的container状态立刻设置为true\n        animateContainerVisibleState = MutableTransitionState(true)\n        // 跳转到index页\n        galleryState.scrollToPage(index)\n        // 等待下一帧\n        ticket.awaitNextTicket()\n        // 关闭loading\n        viewerContainerState?.allowLoading = false\n        // 关闭viewer。打开transform\n        transformSnapToViewer(false)\n        // 开启viewer\n        viewerAlpha.snapTo(1F)\n        // 这两个一起执行\n        listOf(\n            scope.async {\n                // 开启动画\n                transformState?.enterTransform(itemState, animationSpec)\n                // 开启loading\n                viewerContainerState?.allowLoading = true\n            },\n            scope.async {\n                // UI慢慢显示\n                uiAlpha.animateTo(1F, animationSpec)\n            }\n        ).awaitAll()\n        // 执行完成后的回调\n        stateOpenEnd()\n        // 这里需要等待viewer挂载，显示loading界面\n        viewerContainerState?.awaitOpenTransform()\n    }\n\n    /**\n     * 关闭previewer，带转换效果\n     * @param key Any\n     * @param animationSpec AnimationSpec<Float>?\n     */\n    suspend fun closeTransform(\n        animationSpec: AnimationSpec<Float> = defaultAnimationSpec,\n    ) {\n        // 标记开始\n        stateCloseStart()\n        // 判断当前状态是否允许transform结束\n        // 需要在cancel前获取该值\n        val canNowTransformOut = canTransformOut\n        // 关闭可能正在进行的open操作\n        viewerContainerState?.cancelOpenTransform()\n        // 关闭loading的显示\n        viewerContainerState?.allowLoading = false\n        // 查询item是否存在\n        val itemState = findTransformItemByIndex(currentPage)\n        // 如果存在，就transform退出，否则就普通退出\n        if (itemState != null && canNowTransformOut) {\n            // 如果viewer在显示的状态，退出时将viewer的pose复制给content\n            if (viewerContainerVisible) {\n                // 标记transform的开始状态，否则copy无效\n                transformState?.setEnterState()\n                // 更新transformState\n                transformState?.notifyEnterChanged()\n                // 等待刷新完毕\n                ticket.awaitNextTicket()\n                // 复制viewer的pos给transform\n                viewerContainerState?.copyViewerPosToContent(itemState)\n                // 切换为transform\n                transformSnapToViewer(false)\n            }\n            // 等待下一帧\n            ticket.awaitNextTicket()\n            listOf(\n                scope.async {\n                    // transform动画退出\n                    transformState?.exitTransform(animationSpec)\n                    // 退出结束后隐藏content\n                    viewerContainerState?.transformContentAlpha?.snapTo(0F)\n                },\n                scope.async {\n                    // 动画隐藏UI\n                    uiAlpha.animateTo(0F, animationSpec)\n                }\n            ).awaitAll()\n            // 等待下一帧\n            ticket.awaitNextTicket()\n            // 彻底关闭container\n            animateContainerVisibleState = MutableTransitionState(false)\n        } else {\n            // transform标记退出\n            transformState?.setExitState()\n            // container动画退出\n            animateContainerVisibleState.targetState = false\n        }\n        // 允许使用loading\n        viewerContainerState?.allowLoading = true\n        // 标记结束\n        stateCloseEnd()\n    }\n\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/previewer/PreviewerVerticalDragState.kt",
    "content": "package com.origeek.imageViewer.previewer\n\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.MutableTransitionState\nimport androidx.compose.foundation.gestures.detectVerticalDragGestures\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport com.origeek.imageViewer.gallery.ImageGalleryState\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.launch\nimport kotlin.math.absoluteValue\n\n/**\n * @program: ImageViewer\n *\n * @description:\n *\n * @author: JVZIYAOYAO\n *\n * @create: 2022-10-17 14:42\n **/\n\n// 默认下拉关闭缩放阈值\nconst val DEFAULT_SCALE_TO_CLOSE_MIN_VALUE = 0.9F\n\nenum class VerticalDragType {\n    // 不开启垂直手势\n    None,\n\n    // 仅开启下拉手势\n    Down,\n\n    // 支持上下拉手势\n    UpAndDown,\n    ;\n}\n\n/**\n * 增加垂直方向拖拽的能力\n */\nopen class PreviewerVerticalDragState(\n    // 协程作用域\n    scope: CoroutineScope = MainScope(),\n    // 默认动画窗格\n    defaultAnimationSpec: AnimationSpec<Float> = DEFAULT_SOFT_ANIMATION_SPEC,\n    // 开启垂直手势的类型\n    verticalDragType: VerticalDragType = VerticalDragType.None,\n    // 下拉关闭的缩小的阈值\n    scaleToCloseMinValue: Float = DEFAULT_SCALE_TO_CLOSE_MIN_VALUE,\n    // 预览状态\n    galleryState: ImageGalleryState,\n) : PreviewerTransformState(scope, defaultAnimationSpec, galleryState) {\n\n    /**\n     * viewer容器缩小关闭\n     */\n    private suspend fun viewerContainerShrinkDown() {\n        // 标记动作开始\n        stateCloseStart()\n        listOf(\n            // 缩小容器\n            scope.async {\n                viewerContainerState?.scale?.animateTo(0F, animationSpec = defaultAnimationSpec)\n            },\n            // 关闭UI\n            scope.async {\n                uiAlpha.animateTo(0F, animationSpec = defaultAnimationSpec)\n            }\n        ).awaitAll()\n        // 等待下一帧\n        ticket.awaitNextTicket()\n        // 关闭动画组件\n        animateContainerVisibleState = MutableTransitionState(false)\n        // 等待下一帧\n        ticket.awaitNextTicket()\n        // 重置container\n        viewerContainerState?.reset(defaultAnimationSpec)\n        // 将transform标记为退出\n        transformState?.setExitState()\n        // 标记动作结束\n        stateCloseEnd()\n    }\n\n    /**\n     * 响应下拉关闭\n     */\n    private suspend fun dragDownClose() {\n        // 刷新transform的pos\n        transformState?.notifyEnterChanged()\n        // 关闭loading\n        viewerContainerState?.allowLoading = false\n        // 等待下一帧，确保transform的pos刷新成功\n        ticket.awaitNextTicket()\n        // 将container的pos复制给transform\n        viewerContainerState?.copyViewerContainerStateToTransformState()\n        // container重置\n        viewerContainerState?.resetImmediately()\n        // 切换到transform\n        transformSnapToViewer(false)\n        // 等待下一帧\n        ticket.awaitNextTicket()\n        // 执行转换关闭\n        closeTransform(defaultAnimationSpec)\n        // 解除loading限制\n        viewerContainerState?.allowLoading = true\n    }\n\n    /**\n     * 设置下拉手势的方法\n     * @param pointerInputScope PointerInputScope\n     */\n    internal suspend fun verticalDrag(pointerInputScope: PointerInputScope) {\n        pointerInputScope.apply {\n            // 记录开始时的位置\n            var vStartOffset by mutableStateOf<Offset?>(null)\n            // 标记是否为下拉关闭\n            var vOrientationDown by mutableStateOf<Boolean?>(null)\n            // 如果getKay不为空才开始检测手势\n            if (verticalDragType != VerticalDragType.None) detectVerticalDragGestures(\n                onDragStart = OnDragStart@{\n                    // 如果imageViewerState不存在，无法进行下拉手势\n                    if (imageViewerState == null) return@OnDragStart\n                    var transformItemState: TransformItemState? = null\n                    // 查询当前transformItem\n                    getKey?.apply {\n                        findTransformItem(invoke(currentPage))?.apply {\n                            transformItemState = this\n                        }\n                    }\n                    // 判断是否允许变换退出，如果允许就标记动作开始\n                    // setExitState后，在下拉过程中，itemState不会从界面上消失\n                    if (canTransformOut) {\n                        transformState?.setEnterState()\n                    } else {\n                        transformState?.setExitState()\n                    }\n                    // 更新当前transformItem\n                    transformState?.itemState = transformItemState\n                    // 只有viewer的缩放率为1时才允许下拉手势\n                    if (imageViewerState?.scale?.value == 1F) {\n                        vStartOffset = it\n                        // 进入下拉手势时禁用viewer的手势\n                        imageViewerState?.allowGestureInput = false\n                    }\n                },\n                onDragEnd = OnDragEnd@{\n                    // 如果开始位置为空，就退出\n                    if (vStartOffset == null) return@OnDragEnd\n                    // 如果containerState为空，就退出\n                    if (viewerContainerState == null) return@OnDragEnd\n                    // 重置开始位置和方向\n                    vStartOffset = null\n                    vOrientationDown = null\n                    // 解除viewer的手势输入限制\n                    imageViewerState?.allowGestureInput = true\n                    // 缩放小于阈值，执行关闭动画，大于就恢复原样\n                    if (viewerContainerState!!.scale.value < scaleToCloseMinValue) {\n                        scope.launch {\n                            if (getKey != null && canTransformOut) {\n                                val key = getKey!!.invoke(currentPage)\n                                val transformItem = findTransformItem(key)\n                                // 如果item在画面内，就执行变换关闭，否则缩小关闭\n                                if (transformItem != null) {\n                                    dragDownClose()\n                                } else {\n                                    viewerContainerShrinkDown()\n                                }\n                            } else {\n                                viewerContainerShrinkDown()\n                            }\n                            // 结束动画后需要把关闭的UI打开\n                            uiAlpha.snapTo(1F)\n                        }\n                    } else {\n                        scope.launch {\n                            uiAlpha.animateTo(1F, defaultAnimationSpec)\n                        }\n                        scope.launch {\n                            viewerContainerState?.reset(defaultAnimationSpec)\n                        }\n                    }\n                },\n                onVerticalDrag = OnVerticalDrag@{ change, dragAmount ->\n                    if (imageViewerState == null) return@OnVerticalDrag\n                    if (viewerContainerState == null) return@OnVerticalDrag\n                    if (vStartOffset == null) return@OnVerticalDrag\n                    if (vOrientationDown == null) vOrientationDown = dragAmount > 0\n                    if (vOrientationDown == true || verticalDragType == VerticalDragType.UpAndDown) {\n                        val offsetY = change.position.y - vStartOffset!!.y\n                        val offsetX = change.position.x - vStartOffset!!.x\n                        val containerHeight = viewerContainerState!!.containerSize.height\n                        val scale = (containerHeight - offsetY.absoluteValue).div(\n                            containerHeight\n                        )\n                        scope.launch {\n                            uiAlpha.snapTo(scale)\n                            viewerContainerState?.offsetX?.snapTo(offsetX)\n                            viewerContainerState?.offsetY?.snapTo(offsetY)\n                            viewerContainerState?.scale?.snapTo(scale)\n                        }\n                    } else {\n                        // 如果不是向上，就返还输入权，以免页面卡顿\n                        imageViewerState?.allowGestureInput = true\n                    }\n                }\n            )\n        }\n    }\n\n    /**\n     * 开启垂直手势的类型\n     */\n    var verticalDragType by mutableStateOf(verticalDragType)\n\n    /**\n     * 下拉关闭的缩放的阈值，当scale小于这个值，就关闭，否则还原\n     */\n    var scaleToCloseMinValue by mutableStateOf(scaleToCloseMinValue)\n\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/util/Ticket.kt",
    "content": "package com.origeek.imageViewer.util\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport java.util.UUID\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\n\n/**\n * @program: ImageViewer\n *\n * @description:\n *\n * @author: JVZIYAOYAO\n *\n * @create: 2023-05-24 12:15\n **/\nclass Ticket {\n\n    private var ticket by mutableStateOf(\"\")\n\n    private val ticketMap = mutableMapOf<String, Continuation<Unit>>()\n\n    suspend fun awaitNextTicket() = suspendCoroutine<Unit> { c ->\n        ticket = UUID.randomUUID().toString()\n        ticketMap[ticket] = c\n    }\n\n    private fun clearTicket() {\n        ticketMap.forEach {\n            it.value.resume(Unit)\n            ticketMap.remove(it.key)\n        }\n    }\n\n    @Composable\n    fun Next() {\n        LaunchedEffect(ticket) {\n            clearTicket()\n        }\n    }\n\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/viewer/ImageComposeCanvas.kt",
    "content": "package com.origeek.imageViewer.viewer\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.BitmapRegionDecoder\nimport android.graphics.Matrix\nimport android.graphics.Rect\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.graphics.drawscope.withTransform\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.IntSize\nimport com.origeek.imageViewer.previewer.DEFAULT_CROSS_FADE_ANIMATE_SPEC\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.launch\nimport java.math.BigDecimal\nimport java.math.RoundingMode\nimport java.util.concurrent.LinkedBlockingDeque\nimport kotlin.math.absoluteValue\nimport kotlin.math.ceil\n\ndata class RenderBlock(\n    var inBound: Boolean = false,\n    var inSampleSize: Int = 1,\n    var renderOffset: IntOffset = IntOffset.Zero,\n    var renderSize: IntSize = IntSize.Zero,\n    var sliceRect: Rect = Rect(0, 0, 0, 0),\n    private var bitmap: Bitmap? = null,\n) {\n\n    fun release() {\n        bitmap?.recycle()\n        bitmap = null\n    }\n\n    fun getBitmap(): Bitmap? {\n        return bitmap\n    }\n\n    fun setBitmap(bitmap: Bitmap) {\n        this.bitmap = bitmap\n    }\n\n}\n\nval ROTATION_0 = 0\nval ROTATION_90 = 90\nval ROTATION_180 = 180\nval ROTATION_270 = 270\n\nclass RotationIllegalException(msg: String = \"Illegal rotation angle.\") : RuntimeException(msg)\n\nclass ImageDecoder(\n    private val decoder: BitmapRegionDecoder,\n    private val rotation: Int = ROTATION_0,\n    private val onRelease: () -> Unit = {},\n) : CoroutineScope by MainScope() {\n\n    // 解码的宽度\n    var decoderWidth by mutableStateOf(0)\n        private set\n\n    // 解码的高度\n    var decoderHeight by mutableStateOf(0)\n        private set\n\n    // 解码区块大小\n    var blockSize by mutableStateOf(0)\n        private set\n\n    // 渲染列表\n    var renderList: Array<Array<RenderBlock>> = emptyArray()\n        private set\n\n    // 解码渲染队列\n    val renderQueue = LinkedBlockingDeque<RenderBlock>()\n\n    // 横向方块数\n    private var countW = 0\n\n    // 纵向方块数\n    private var countH = 0\n\n    // 最长边的最大方块数\n    private var maxBlockCount = 0\n\n    init {\n        // 初始化最大方块数\n        setMaxBlockCount(1)\n    }\n\n    // 构造一个渲染方块队列\n    private fun getRenderBlockList(): Array<Array<RenderBlock>> {\n        var endX: Int\n        var endY: Int\n        var sliceStartX: Int\n        var sliceStartY: Int\n        var sliceEndX: Int\n        var sliceEndY: Int\n        return Array(countH) { column ->\n            sliceStartY = (column * blockSize)\n            endY = (column + 1) * blockSize\n            sliceEndY = if (endY > decoderHeight) decoderHeight else endY\n            Array(countW) { row ->\n                sliceStartX = (row * blockSize)\n                endX = (row + 1) * blockSize\n                sliceEndX = if (endX > decoderWidth) decoderWidth else endX\n                RenderBlock(\n                    sliceRect = Rect(\n                        sliceStartX,\n                        sliceStartY,\n                        sliceEndX,\n                        sliceEndY,\n                    )\n                )\n            }\n        }\n    }\n\n    // 设置最长边最大方块数\n    fun setMaxBlockCount(count: Int): Boolean {\n        if (maxBlockCount == count) return false\n        if (decoder.isRecycled) return false\n\n        when (rotation) {\n            ROTATION_0, ROTATION_180 -> {\n                decoderWidth = decoder.width\n                decoderHeight = decoder.height\n            }\n\n            ROTATION_90, ROTATION_270 -> {\n                decoderWidth = decoder.height\n                decoderHeight = decoder.width\n            }\n\n            else -> throw RotationIllegalException()\n        }\n\n        maxBlockCount = count\n        blockSize =\n            (decoderWidth.coerceAtLeast(decoderHeight)).toFloat().div(count).toInt()\n        countW = ceil(decoderWidth.toFloat().div(blockSize)).toInt()\n        countH = ceil(decoderHeight.toFloat().div(blockSize)).toInt()\n        renderList = getRenderBlockList()\n        return true\n    }\n\n    // 遍历每一个渲染方块\n    fun forEachBlock(action: (block: RenderBlock, column: Int, row: Int) -> Unit) {\n        for ((column, rows) in renderList.withIndex()) {\n            for ((row, block) in rows.withIndex()) {\n                action(block, column, row)\n            }\n        }\n    }\n\n    // 清除全部bitmap的引用\n    fun clearAllBitmap() {\n        forEachBlock { block, _, _ ->\n            block.release()\n        }\n    }\n\n    // 释放资源\n    fun release() {\n        synchronized(decoder) {\n            if (!decoder.isRecycled) {\n                // 清除渲染队列\n                renderQueue.clear()\n                // 回收资源\n                decoder.recycle()\n                // 发送一个信号停止堵塞的循环\n                renderQueue.putFirst(RenderBlock())\n            }\n            onRelease()\n        }\n    }\n\n    fun getRotateBitmap(bitmap: Bitmap, degree: Float): Bitmap {\n        val matrix = Matrix()\n        matrix.postRotate(degree)\n        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)\n    }\n\n    /**\n     * 解码渲染区域\n     */\n    fun decodeRegion(inSampleSize: Int, rect: Rect): Bitmap? {\n        synchronized(decoder) {\n            return try {\n                val ops = BitmapFactory.Options()\n                ops.inSampleSize = inSampleSize\n                if (decoder.isRecycled) return null\n                return if (rotation == ROTATION_0) {\n                    decoder.decodeRegion(rect, ops)\n                } else {\n                    val newRect = when (rotation) {\n                        ROTATION_90 -> {\n                            val nextX1 = rect.top\n                            val nextX2 = rect.bottom\n                            val nextY1 = decoderWidth - rect.right\n                            val nextY2 = decoderWidth - rect.left\n                            Rect(nextX1, nextY1, nextX2, nextY2)\n                        }\n\n                        ROTATION_180 -> {\n                            val nextX1 = decoderWidth - rect.right\n                            val nextX2 = decoderWidth - rect.left\n                            val nextY1 = decoderHeight - rect.bottom\n                            val nextY2 = decoderHeight - rect.top\n                            Rect(nextX1, nextY1, nextX2, nextY2)\n                        }\n\n                        ROTATION_270 -> {\n                            val nextX1 = decoderHeight - rect.bottom\n                            val nextX2 = decoderHeight - rect.top\n                            val nextY1 = rect.left\n                            val nextY2 = rect.right\n                            Rect(nextX1, nextY1, nextX2, nextY2)\n                        }\n\n                        else -> throw RotationIllegalException()\n                    }\n                    val srcBitmap = decoder.decodeRegion(newRect, ops)\n                    getRotateBitmap(bitmap = srcBitmap, rotation.toFloat())\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n                null\n            }\n        }\n    }\n\n    // 开启堵塞队列的循环\n    fun startRenderQueue(onUpdate: () -> Unit) {\n        launch(Dispatchers.IO) {\n            try {\n                while (!decoder.isRecycled) {\n                    val block = renderQueue.take()\n                    if (decoder.isRecycled) break\n                    val bitmap = decodeRegion(block.inSampleSize, block.sliceRect)\n                    if (bitmap != null) block.setBitmap(bitmap)\n                    onUpdate()\n                }\n            } catch (e: InterruptedException) {\n                e.printStackTrace()\n            }\n        }\n    }\n}\n\n@Composable\nfun ImageComposeCanvas(\n    modifier: Modifier = Modifier,\n    imageDecoder: ImageDecoder,\n    scale: Float = DEFAULT_SCALE,\n    offsetX: Float = DEFAULT_OFFSET_X,\n    offsetY: Float = DEFAULT_OFFSET_Y,\n    rotation: Float = DEFAULT_ROTATION,\n    gesture: RawGesture = RawGesture(),\n    onMounted: () -> Unit = {},\n    onSizeChange: suspend (SizeChangeContent) -> Unit = {},\n    crossfadeAnimationSpec: AnimationSpec<Float> = DEFAULT_CROSS_FADE_ANIMATE_SPEC,\n    boundClip: Boolean = true,\n    debugMode: Boolean = false,\n) {\n    val scope = rememberCoroutineScope()\n    // 容器大小\n    var bSize by remember { mutableStateOf(IntSize.Zero) }\n    // 容器长宽比\n    val bRatio by remember { derivedStateOf { bSize.width.toFloat() / bSize.height.toFloat() } }\n    // 原图长宽比\n    val oRatio by remember { derivedStateOf { imageDecoder.decoderWidth.toFloat() / imageDecoder.decoderHeight.toFloat() } }\n    // 是否宽度与容器大小一致\n    var widthFixed by remember { mutableStateOf(false) }\n    // 长宽是否均超出容器长宽\n    val superSize by remember {\n        derivedStateOf {\n            imageDecoder.decoderHeight > bSize.height && imageDecoder.decoderWidth > bSize.width\n        }\n    }\n    // 显示的默认大小\n    val uSize by remember {\n        derivedStateOf {\n            if (oRatio > bRatio) {\n                // 宽度一致\n                val uW = bSize.width\n                val uH = uW / oRatio\n                widthFixed = true\n                IntSize(uW, uH.toInt())\n            } else {\n                // 高度一致\n                val uH = bSize.height\n                val uW = uH * oRatio\n                widthFixed = false\n                IntSize(uW.toInt(), uH)\n            }\n        }\n    }\n    // 显示的实际大小\n    val rSize by remember(key1 = scale) {\n        derivedStateOf {\n            IntSize(\n                (uSize.width * scale).toInt(),\n                (uSize.height * scale).toInt()\n            )\n        }\n    }\n\n    // 同时监听容器和实际图片大小的变化\n    LaunchedEffect(key1 = bSize, key2 = rSize) {\n        // 获取最大缩放率\n        val maxScale = when {\n            superSize -> {\n                imageDecoder.decoderWidth.toFloat() / uSize.width.toFloat()\n            }\n\n            widthFixed -> {\n                bSize.height.toFloat() / uSize.height.toFloat()\n            }\n\n            else -> {\n                bSize.width.toFloat() / uSize.width.toFloat()\n            }\n        }\n        // 回调\n        onSizeChange(\n            SizeChangeContent(\n                defaultSize = uSize,\n                containerSize = bSize,\n                maxScale = maxScale\n            )\n        )\n    }\n\n    // 判断是否需要高画质渲染\n    val needRenderHeightTexture by remember(key1 = bSize) {\n        derivedStateOf {\n            // 目前策略：原图的面积大于容器面积，就要渲染高画质\n            BigDecimal(imageDecoder.decoderWidth)\n                .multiply(BigDecimal(imageDecoder.decoderHeight)) > BigDecimal(bSize.height)\n                .multiply(BigDecimal(bSize.width))\n        }\n    }\n    // 标识当前是否开启高画质渲染，如果需要高画质渲染，并且缩放大于1\n    val renderHeightTexture by remember(key1 = scale) { derivedStateOf { needRenderHeightTexture && scale > 1 } }\n    // 当前采样率\n    var inSampleSize by remember { mutableStateOf(1) }\n    // 最小图的采样率\n    var zeroInSampleSize by remember { mutableStateOf(8) }\n    // 底图的采样率\n    var backGroundInSample by remember { mutableStateOf(0) }\n    // 底图bitmap\n    var bitmap by remember { mutableStateOf<Bitmap?>(null) }\n    // 监听渲染实际大小，动态修改图片的采样率\n    LaunchedEffect(key1 = rSize) {\n        if (scale < 1F) return@LaunchedEffect\n        inSampleSize = calculateInSampleSize(\n            srcWidth = imageDecoder.decoderWidth,\n            reqWidth = rSize.width\n        )\n        if (scale == 1F) {\n            zeroInSampleSize = inSampleSize\n        }\n    }\n    // 根据采样率变化，实时更新底图\n    LaunchedEffect(key1 = zeroInSampleSize, key2 = inSampleSize, key3 = needRenderHeightTexture) {\n        scope.launch(Dispatchers.IO) {\n            // 如果不需要渲染高画质，就不需要分块渲染，直接使用当前采样率，用底图来展示\n            val iss = if (needRenderHeightTexture) zeroInSampleSize else inSampleSize\n            if (iss == backGroundInSample) return@launch\n            backGroundInSample = iss\n            bitmap = imageDecoder.decodeRegion(\n                iss, Rect(\n                    0,\n                    0,\n                    imageDecoder.decoderWidth,\n                    imageDecoder.decoderHeight\n                )\n            )\n        }\n    }\n    DisposableEffect(Unit) {\n        onDispose {\n            bitmap?.recycle()\n            bitmap = null\n        }\n    }\n\n    // 底图偏移量X，要确保图片在容器中居中对齐\n    val deltaX by remember(key1 = offsetX, key2 = bSize, key3 = rSize) {\n        derivedStateOf {\n            offsetX + (bSize.width - rSize.width).toFloat().div(2)\n        }\n    }\n    // 底图偏移量Y，要确保图片在容器中居中对齐\n    val deltaY by remember(key1 = offsetY, key2 = bSize, key3 = rSize) {\n        derivedStateOf {\n            offsetY + (bSize.height - rSize.height).toFloat().div(2)\n        }\n    }\n    // 计算显示区域内矩形的宽度\n    val rectW by remember(key1 = offsetX) {\n        derivedStateOf {\n            calcLeftSize(\n                bSize = bSize.width.toFloat(),\n                rSize = rSize.width.toFloat(),\n                offset = offsetX,\n            )\n        }\n    }\n    // 计算显示区域内矩形的高度\n    val rectH by remember(key1 = offsetY, key2 = rSize) {\n        derivedStateOf {\n            calcLeftSize(\n                bSize = bSize.height.toFloat(),\n                rSize = rSize.height.toFloat(),\n                offset = offsetY,\n            )\n        }\n    }\n    // 渲染可见区域的开始坐标X\n    val stX by remember(key1 = offsetX) {\n        derivedStateOf {\n            // 计算显示区域矩形的偏移坐标\n            val rectDeltaX = getRectDelta(\n                deltaX,\n                rSize.width.toFloat(),\n                bSize.width.toFloat(),\n                offsetX\n            )\n            // 偏移坐标减偏移量求出矩形在图片上的相对坐标\n            rectDeltaX - deltaX\n        }\n    }\n    // 渲染可见区域的开始坐标Y\n    val stY by remember(key1 = offsetY) {\n        derivedStateOf {\n            // 计算显示区域矩形的偏移坐标\n            val rectDeltaY = getRectDelta(\n                deltaY,\n                rSize.height.toFloat(),\n                bSize.height.toFloat(),\n                offsetY\n            )\n            // 偏移坐标减偏移量求出矩形在图片上的相对坐标\n            rectDeltaY - deltaY\n        }\n    }\n    // 开始坐标加上宽度等于结束坐标\n    val edX by remember(key1 = offsetX) { derivedStateOf { stX + rectW } }\n    // 开始坐标加上高度等于结束坐标\n    val edY by remember(key1 = offsetY) { derivedStateOf { stY + rectH } }\n\n    // 更新时间戳，用于通知canvas更新方块\n    var renderUpdateTimeStamp by remember { mutableStateOf(0L) }\n    // 开启解码队列的循环\n    LaunchedEffect(key1 = Unit) {\n        imageDecoder.startRenderQueue {\n            // 解码器解码一个，就更新一次时间戳\n            renderUpdateTimeStamp = System.currentTimeMillis()\n        }\n    }\n    // 切换到不需要高画质渲染时，需要清除解码队列，清除全部的bitmap\n    LaunchedEffect(key1 = renderHeightTexture) {\n        if (!renderHeightTexture) {\n            imageDecoder.renderQueue.clear()\n            imageDecoder.clearAllBitmap()\n        }\n    }\n\n    /**\n     * 更新渲染队列\n     */\n    var calcMaxCountPending by remember { mutableStateOf(false) }\n    // 先前的缩放比\n    var previousScale by remember { mutableStateOf<Float?>(null) }\n    // 先前的偏移量\n    var previousOffset by remember { mutableStateOf<Offset?>(null) }\n\n    // 记录最长边的最大方块数\n    var blockDividerCount by remember { mutableStateOf(1) }\n    // 用来标识这个参数是否有改变\n    var preBlockDividerCount by remember { mutableStateOf(blockDividerCount) }\n\n    // 更新渲染方块的信息\n    fun updateRenderList() {\n        // 如果此时正在重新计算渲染方块的数目，就退出\n        if (calcMaxCountPending) return\n        // 更新的时候如果缩放和偏移量没有变化，方块数量也没变，就没有必要计算了\n        if (\n            previousOffset?.x == offsetX\n            && previousOffset?.y == offsetY\n            && previousScale == scale\n            && preBlockDividerCount == blockDividerCount\n        ) return\n        previousScale = scale\n        previousOffset = Offset(offsetX, offsetY)\n        // 计算当前渲染方块大小\n        val renderBlockSize =\n            imageDecoder.blockSize * (rSize.width.toFloat().div(imageDecoder.decoderWidth))\n        var tlx: Int\n        var tly: Int\n        var startX: Float\n        var startY: Float\n        var endX: Float\n        var endY: Float\n        var eh: Int\n        var ew: Int\n        var needUpdate: Boolean\n        var previousInBound: Boolean\n        var previousInSampleSize: Int\n        var lastX: Int?\n        var lastY: Int? = null\n        var lastXDelta: Int\n        var lastYDelta: Int\n        val insertList = ArrayList<RenderBlock>()\n        val removeList = ArrayList<RenderBlock>()\n        for ((column, list) in imageDecoder.renderList.withIndex()) {\n            startY = column * renderBlockSize\n            endY = (column + 1) * renderBlockSize\n            tly = (deltaY + startY).toInt()\n            eh = (if (endY > rSize.height) rSize.height - startY else renderBlockSize).toInt()\n            // 由于计算的精度问题，需要确保每一个区块都要严丝合缝\n            lastY?.let {\n                if (it < tly) {\n                    lastYDelta = tly - it\n                    tly = it\n                    eh += lastYDelta\n                }\n            }\n            lastY = tly + eh\n            lastX = null\n            for ((row, block) in list.withIndex()) {\n                startX = row * renderBlockSize\n                tlx = (deltaX + startX).toInt()\n                endX = (row + 1) * renderBlockSize\n                ew = (if (endX > rSize.width) rSize.width - startX else renderBlockSize).toInt()\n                previousInSampleSize = block.inSampleSize\n                previousInBound = block.inBound\n                // 记录当前区块的采用率\n                block.inSampleSize = inSampleSize\n                // 判断区块是否在可视范围内\n                block.inBound = checkRectInBound(\n                    startX, startY, endX, endY,\n                    stX, stY, edX, edY\n                )\n                // 由于计算的精度问题，需要确保每一个区块都要严丝合缝\n                lastX?.let {\n                    if (it < tlx) {\n                        lastXDelta = tlx - it\n                        tlx = it\n                        ew += lastXDelta\n                    }\n                }\n                lastX = tlx + ew\n                // 记录区块的实际偏移量\n                block.renderOffset = IntOffset(tlx, tly)\n                // 记录区块的实际大小\n                block.renderSize = IntSize(\n                    width = ew,\n                    height = eh,\n                )\n                // 如果参数跟之前的一样，就没有必要更新bitmap\n                needUpdate = previousInBound != block.inBound\n                        || previousInSampleSize != block.inSampleSize\n                if (!needUpdate) continue\n                if (!renderHeightTexture) continue\n                // 解码队列操作时是有锁的，会对性能造成影响\n                if (block.inBound) {\n                    if (!imageDecoder.renderQueue.contains(block)) {\n                        insertList.add(block)\n                    }\n                } else {\n                    removeList.add(block)\n                    block.release()\n                }\n            }\n        }\n        scope.launch(Dispatchers.IO) {\n            synchronized(imageDecoder.renderQueue) {\n                insertList.forEach {\n                    imageDecoder.renderQueue.putFirst(it)\n                }\n                removeList.forEach {\n                    imageDecoder.renderQueue.remove(it)\n                }\n            }\n        }\n    }\n\n    LaunchedEffect(key1 = rSize, key2 = rectW, key3 = rectH) {\n        // 可视区域面积\n        val rectArea = BigDecimal(rectW.toDouble()).multiply(BigDecimal(rectH.toDouble()))\n        // 实际大小面积\n        val realArea = BigDecimal(rSize.width).multiply(BigDecimal(rSize.height))\n        // 被除数不能为0\n        if (realArea.toFloat() == 0F) return@LaunchedEffect\n        // 计算实际面积的可视率\n        val renderAreaPercentage =\n            rectArea.divide(realArea, 2, RoundingMode.HALF_EVEN).toFloat()\n        // 根据不同可视率，匹配合适的方块数，最大只能到8\n        val goBlockDividerCount = when {\n            renderAreaPercentage > 0.6F -> 1\n            renderAreaPercentage > 0.025F -> 4\n            else -> 8\n        }\n        // 如果没变，就不要修改\n        if (goBlockDividerCount == blockDividerCount) return@LaunchedEffect\n        preBlockDividerCount = blockDividerCount\n        blockDividerCount = goBlockDividerCount\n        scope.launch(Dispatchers.IO) {\n            // 清空解码队列\n            imageDecoder.renderQueue.clear()\n            // 进入修改区间\n            calcMaxCountPending = true\n            imageDecoder.setMaxBlockCount(blockDividerCount)\n            calcMaxCountPending = false\n            // 离开修改区间\n\n            // 更新一下界面\n            updateRenderList()\n        }\n    }\n\n    // 旋转中心\n    val rotationCenter by remember(key1 = offsetX, key2 = offsetY, key3 = scale) {\n        derivedStateOf {\n            val cx = deltaX + rSize.width.div(2)\n            val cy = deltaY + rSize.height.div(2)\n            Offset(cx, cy)\n        }\n    }\n\n    /**\n     * canvas加载成功后避免闪一下\n     */\n    val canvasAlpha = remember { Animatable(0F) }\n    LaunchedEffect(key1 = bitmap) {\n        if (bitmap != null && bitmap!!.width > 1 && bitmap!!.height > 1) {\n            if (canvasAlpha.value == 0F) {\n                scope.launch {\n                    canvasAlpha.animateTo(\n                        targetValue = 1F,\n                        animationSpec = crossfadeAnimationSpec\n                    )\n                    onMounted()\n                }\n            }\n        }\n    }\n\n    Canvas(\n        modifier = modifier\n            .alpha(canvasAlpha.value)\n            .fillMaxSize()\n            .graphicsLayer {\n                // 图片位移时会超出容器大小，需要在这个地方指定是否裁切\n                clip = boundClip\n            }\n            .onSizeChanged {\n                bSize = it\n            }\n            .pointerInput(Unit) {\n                detectTapGestures(onLongPress = gesture.onLongPress)\n            }\n            .pointerInput(Unit) {\n                detectTransformGestures(\n                    onTap = gesture.onTap,\n                    onDoubleTap = gesture.onDoubleTap,\n                    gestureStart = gesture.gestureStart,\n                    gestureEnd = gesture.gestureEnd,\n                    onGesture = gesture.onGesture,\n                )\n            },\n    ) {\n        withTransform({\n            rotate(degrees = rotation, pivot = rotationCenter)\n        }) {\n            if (bitmap != null) {\n                drawImage(\n                    image = bitmap!!.asImageBitmap(),\n                    dstSize = IntSize(rSize.width, rSize.height),\n                    dstOffset = IntOffset(deltaX.toInt(), deltaY.toInt()),\n                )\n            }\n            // 更新渲染队列\n            if (renderUpdateTimeStamp >= 0) updateRenderList()\n            if (renderHeightTexture && !calcMaxCountPending) {\n                imageDecoder.forEachBlock { block, _, _ ->\n                    block.getBitmap()?.let {\n                        drawImage(\n                            image = it.asImageBitmap(),\n                            dstSize = block.renderSize,\n                            dstOffset = block.renderOffset\n                        )\n                    }\n                }\n            }\n            // 这里会把可视区域的矩形画出来\n            if (debugMode) {\n                drawRect(\n                    color = Color.Blue.copy(0.1F),\n                    topLeft = Offset(deltaX + stX, deltaY + stY),\n                    size = Size(rectW, rectH)\n                )\n            }\n        }\n    }\n}\n\nfun checkRectInBound(\n    stX1: Float, stY1: Float, edX1: Float, edY1: Float,\n    stX2: Float, stY2: Float, edX2: Float, edY2: Float,\n): Boolean {\n    if (edY1 < stY2) return false\n    if (stY1 > edY2) return false\n    if (edX1 < stX2) return false\n    if (stX1 > edX2) return false\n    return true\n}\n\nfun getRectDelta(delta: Float, rSize: Float, bSize: Float, offset: Float): Float {\n    return delta + if (delta < 0) {\n        val direction = if (rSize > bSize) -1 else 1\n        (offset + (direction) * (bSize - rSize)\n            .div(2).absoluteValue).absoluteValue\n    } else 0F\n}\n\nfun calcLeftSize(bSize: Float, rSize: Float, offset: Float): Float {\n    return if (offset.absoluteValue > (bSize - rSize).div(2).absoluteValue) {\n        rSize - (offset.absoluteValue - (bSize - rSize).div(2))\n    } else {\n        rSize.coerceAtMost(bSize)\n    }\n}\n\nfun calculateInSampleSize(\n    srcWidth: Int,\n    reqWidth: Int,\n): Int {\n    var inSampleSize = 1\n    while (true) {\n        val iss = inSampleSize * 2\n        if (srcWidth.toFloat().div(iss) < reqWidth) break\n        inSampleSize = iss\n    }\n    return inSampleSize\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/viewer/ImageComposeOrigin.kt",
    "content": "package com.origeek.imageViewer.viewer\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.isSpecified\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.input.pointer.PointerEvent\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.IntSize\nimport com.origeek.imageViewer.previewer.DEFAULT_CROSS_FADE_ANIMATE_SPEC\nimport kotlinx.coroutines.launch\n\nclass RawGesture(\n    val onTap: (Offset) -> Unit = {},\n    val onDoubleTap: (Offset) -> Unit = {},\n    val onLongPress: (Offset) -> Unit = {},\n    val gestureStart: () -> Unit = {},\n    val gestureEnd: (transformOnly: Boolean) -> Unit = {},\n    val onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float, event: PointerEvent) -> Boolean = { _, _, _, _, _ -> true },\n)\n\ndata class SizeChangeContent(\n    val defaultSize: IntSize,\n    val containerSize: IntSize,\n    val maxScale: Float,\n)\n\n@Composable\nfun ImageComposeOrigin(\n    modifier: Modifier = Modifier,\n    model: Any,\n    scale: Float = DEFAULT_SCALE,\n    offsetX: Float = DEFAULT_OFFSET_X,\n    offsetY: Float = DEFAULT_OFFSET_Y,\n    rotation: Float = DEFAULT_ROTATION,\n    gesture: RawGesture = RawGesture(),\n    onMounted: () -> Unit = {},\n    onSizeChange: suspend (SizeChangeContent) -> Unit = {},\n    crossfadeAnimationSpec: AnimationSpec<Float> = DEFAULT_CROSS_FADE_ANIMATE_SPEC,\n    boundClip: Boolean = true,\n) {\n    val scope = rememberCoroutineScope()\n    // 容器大小\n    var bSize by remember { mutableStateOf(IntSize(0, 0)) }\n    // 容器比例\n    val bRatio by remember { derivedStateOf { bSize.width.toFloat() / bSize.height.toFloat() } }\n    // 图片原始大小\n    var oSize by remember { mutableStateOf(IntSize(0, 0)) }\n    // 图片原始比例\n    val oRatio by remember { derivedStateOf { oSize.width.toFloat() / oSize.height.toFloat() } }\n    // 是否宽度与容器大小一致\n    var widthFixed by remember { mutableStateOf(false) }\n    // 长宽是否均超出容器长宽\n    val superSize by remember {\n        derivedStateOf {\n            oSize.height > bSize.height && oSize.width > bSize.width\n        }\n    }\n    // 显示大小\n    val uSize by remember {\n        derivedStateOf {\n            if (oRatio > bRatio) {\n                // 宽度一致\n                val uW = bSize.width\n                val uH = uW / oRatio\n                widthFixed = true\n                IntSize(uW, uH.toInt())\n            } else {\n                // 高度一致\n                val uH = bSize.height\n                val uW = uH * oRatio\n                widthFixed = false\n                IntSize(uW.toInt(), uH)\n            }\n        }\n    }\n    // 图片显示的真实大小\n    val rSize by remember {\n        derivedStateOf {\n            IntSize(\n                (uSize.width * scale).toInt(),\n                (uSize.height * scale).toInt()\n            )\n        }\n    }\n\n    LaunchedEffect(key1 = oSize, key2 = bSize, key3 = rSize) {\n        val maxScale = when {\n            superSize -> {\n                oSize.width.toFloat() / uSize.width.toFloat()\n            }\n\n            widthFixed -> {\n                bSize.height.toFloat() / uSize.height.toFloat()\n            }\n\n            else -> {\n                bSize.width.toFloat() / uSize.width.toFloat()\n            }\n        }\n        onSizeChange(\n            SizeChangeContent(\n                defaultSize = uSize,\n                containerSize = bSize,\n                maxScale = maxScale\n            )\n        )\n    }\n\n    // 图片是否加载成功\n    var imageSpecified by remember { mutableStateOf(false) }\n\n    // 承载容器的透明度，主要用来控制图片加载成功后的渐变效果\n    val viewerAlpha = remember { Animatable(0F) }\n\n    /**\n     * mounted回调\n     */\n    fun goMounted() {\n        scope.launch {\n            viewerAlpha.animateTo(1F, crossfadeAnimationSpec)\n            onMounted()\n        }\n    }\n\n    when (model) {\n        is Painter -> {\n            var isMounted by remember { mutableStateOf(false) }\n            imageSpecified = model.intrinsicSize.isSpecified\n            LaunchedEffect(key1 = model.intrinsicSize, block = {\n                if (imageSpecified) {\n                    oSize = IntSize(\n                        model.intrinsicSize.width.toInt(),\n                        model.intrinsicSize.height.toInt()\n                    )\n                    if (!isMounted) {\n                        isMounted = true\n                        goMounted()\n                    }\n                }\n            })\n        }\n\n        is ImageVector -> {\n            imageSpecified = true\n            LocalDensity.current.run {\n                oSize = IntSize(\n                    model.defaultWidth.toPx().toInt(),\n                    model.defaultHeight.toPx().toInt(),\n                )\n                goMounted()\n            }\n        }\n\n        is ImageBitmap -> {\n            imageSpecified = true\n            oSize = IntSize(\n                model.width,\n                model.height\n            )\n            goMounted()\n        }\n\n        is ComposeModel -> {\n            imageSpecified = true\n            LaunchedEffect(key1 = model.intrinsicSize, block = {\n                oSize = if (model.intrinsicSize == IntSize.Zero) {\n                    bSize\n                } else {\n                    model.intrinsicSize\n                }\n            })\n            goMounted()\n        }\n\n        else -> throw Exception(\"This model type is not supported!\")\n    }\n\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n            .graphicsLayer {\n                // 图片位移时会超出容器大小，需要在这个地方指定是否裁切\n                clip = boundClip\n                alpha = viewerAlpha.value\n            }\n            .onSizeChanged {\n                bSize = it\n            }\n            .pointerInput(Unit) {\n                detectTapGestures(onLongPress = gesture.onLongPress)\n            }\n            .pointerInput(key1 = imageSpecified) {\n                if (imageSpecified) detectTransformGestures(\n                    onTap = gesture.onTap,\n                    onDoubleTap = gesture.onDoubleTap,\n                    gestureStart = gesture.gestureStart,\n                    gestureEnd = gesture.gestureEnd,\n                    onGesture = gesture.onGesture,\n                )\n            },\n        contentAlignment = Alignment.Center,\n    ) {\n        val imageModifier = Modifier\n            .graphicsLayer {\n                if (imageSpecified) {\n                    scaleX = scale\n                    scaleY = scale\n                    translationX = offsetX\n                    translationY = offsetY\n                    rotationZ = rotation\n                }\n            }\n            .size(\n                LocalDensity.current.run { uSize.width.toDp() },\n                LocalDensity.current.run { uSize.height.toDp() }\n            )\n        when (model) {\n            is Painter -> {\n                Image(\n                    painter = model,\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop,\n                    modifier = imageModifier,\n                )\n            }\n\n            is ImageVector -> {\n                Image(\n                    imageVector = model,\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop,\n                    modifier = imageModifier,\n                )\n            }\n\n            is ImageBitmap -> {\n                Image(\n                    bitmap = model,\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop,\n                    modifier = imageModifier,\n                )\n            }\n\n            is ComposeModel -> {\n                Box(modifier = imageModifier, contentAlignment = Alignment.Center) {\n                    model.PoseContent()\n                }\n            }\n        }\n\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/com/origeek/imageViewer/viewer/ImageViewer.kt",
    "content": "package com.origeek.imageViewer.viewer\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.FloatExponentialDecaySpec\nimport androidx.compose.animation.core.SpringSpec\nimport androidx.compose.animation.core.generateDecayAnimationSpec\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.gestures.calculateCentroid\nimport androidx.compose.foundation.gestures.calculateCentroidSize\nimport androidx.compose.foundation.gestures.calculatePan\nimport androidx.compose.foundation.gestures.calculateRotation\nimport androidx.compose.foundation.gestures.calculateZoom\nimport androidx.compose.foundation.gestures.forEachGesture\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.input.pointer.PointerEvent\nimport androidx.compose.ui.input.pointer.PointerEventType\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport androidx.compose.ui.input.pointer.consumeAllChanges\nimport androidx.compose.ui.input.pointer.positionChangeConsumed\nimport androidx.compose.ui.input.pointer.positionChanged\nimport androidx.compose.ui.input.pointer.util.VelocityTracker\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastAny\nimport androidx.compose.ui.util.fastForEach\nimport androidx.compose.ui.zIndex\nimport com.origeek.imageViewer.previewer.DEFAULT_CROSS_FADE_ANIMATE_SPEC\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\nimport kotlin.math.PI\nimport kotlin.math.abs\nimport kotlin.math.absoluteValue\n\n// 默认X轴偏移量\nconst val DEFAULT_OFFSET_X = 0F\n\n// 默认Y轴偏移量\nconst val DEFAULT_OFFSET_Y = 0F\n\n// 默认缩放率\nconst val DEFAULT_SCALE = 1F\n\n// 默认旋转角度\nconst val DEFAULT_ROTATION = 0F\n\n// 图片最小缩放率\nconst val MIN_SCALE = 0.5F\n\n// 图片最大缩放率\nconst val MAX_SCALE_RATE = 3.2F\n\n// 最小手指手势间距\nconst val MIN_GESTURE_FINGER_DISTANCE = 200\n\n/**\n * viewer状态对象，用于记录compose组件状态\n */\nclass ImageViewerState(\n    // X轴偏移量\n    offsetX: Float = DEFAULT_OFFSET_X,\n    // Y轴偏移量\n    offsetY: Float = DEFAULT_OFFSET_Y,\n    // 缩放率\n    scale: Float = DEFAULT_SCALE,\n    // 旋转角度\n    rotation: Float = DEFAULT_ROTATION,\n    // 动画窗格\n    animationSpec: AnimationSpec<Float>? = null,\n    // 淡入淡出效果\n    crossfadeAnimationSpec: AnimationSpec<Float>? = null,\n) : CoroutineScope by MainScope() {\n\n    // 默认动画窗格\n    var defaultAnimateSpec: AnimationSpec<Float> = animationSpec ?: SpringSpec()\n\n    // viewer挂载成功后显示时的动画窗格\n    var crossfadeAnimationSpec: AnimationSpec<Float> =\n        crossfadeAnimationSpec ?: DEFAULT_CROSS_FADE_ANIMATE_SPEC\n\n    // x偏移\n    val offsetX = Animatable(offsetX)\n\n    // y偏移\n    val offsetY = Animatable(offsetY)\n\n    // 放大倍率\n    val scale = Animatable(scale)\n\n    // 旋转\n    val rotation = Animatable(rotation)\n\n    // 是否允许手势输入\n    var allowGestureInput by mutableStateOf(true)\n\n    // 默认显示大小\n    var defaultSize by mutableStateOf(IntSize(0, 0))\n        internal set\n\n    // 容器大小\n    internal var containerSize by mutableStateOf(IntSize(0, 0))\n\n    // 最大缩放\n    internal var maxScale by mutableStateOf(1F)\n\n    // 标识是否来自saver，旋转屏幕后会变成true\n    internal var fromSaver = false\n\n    // 恢复的时间戳\n    internal var resetTimeStamp by mutableStateOf(0L)\n\n    // 挂载状态\n    internal val mountedFlow = MutableStateFlow(false)\n\n    /**\n     * 判断是否有动画正在运行\n     * @return Boolean\n     */\n    internal fun isRunning(): Boolean {\n        return scale.isRunning\n                || offsetX.isRunning\n                || offsetY.isRunning\n                || rotation.isRunning\n    }\n\n    /**\n     * 立即设置回初始值\n     */\n    suspend fun resetImmediately() {\n        rotation.snapTo(DEFAULT_ROTATION)\n        offsetX.snapTo(DEFAULT_OFFSET_X)\n        offsetY.snapTo(DEFAULT_OFFSET_Y)\n        scale.snapTo(DEFAULT_SCALE)\n    }\n\n    /**\n     * 设置回初始值\n     */\n    suspend fun reset(animationSpec: AnimationSpec<Float> = defaultAnimateSpec) {\n        coroutineScope {\n            launch {\n                rotation.animateTo(DEFAULT_ROTATION, animationSpec)\n                resetTimeStamp = System.currentTimeMillis()\n            }\n            launch {\n                offsetX.animateTo(DEFAULT_OFFSET_X, animationSpec)\n                resetTimeStamp = System.currentTimeMillis()\n            }\n            launch {\n                offsetY.animateTo(DEFAULT_OFFSET_Y, animationSpec)\n                resetTimeStamp = System.currentTimeMillis()\n            }\n            launch {\n                scale.animateTo(DEFAULT_SCALE, animationSpec)\n                resetTimeStamp = System.currentTimeMillis()\n            }\n        }\n    }\n\n    /**\n     * 放大到最大\n     */\n    suspend fun scaleToMax(\n        offset: Offset,\n        animationSpec: AnimationSpec<Float>? = null\n    ) {\n        val currentAnimateSpec = animationSpec ?: defaultAnimateSpec\n        // 计算x和y偏移量和范围，并确保不会在放大过程中超出范围\n        var bcx = (containerSize.width / 2 - offset.x) * maxScale\n        val boundX = getBound(defaultSize.width.toFloat() * maxScale, containerSize.width.toFloat())\n        bcx = limitToBound(bcx, boundX)\n        var bcy = (containerSize.height / 2 - offset.y) * maxScale\n        val boundY =\n            getBound(defaultSize.height.toFloat() * maxScale, containerSize.height.toFloat())\n        bcy = limitToBound(bcy, boundY)\n        // 启动\n        coroutineScope {\n            launch {\n                scale.animateTo(maxScale, currentAnimateSpec)\n            }\n            launch {\n                offsetX.animateTo(bcx, currentAnimateSpec)\n            }\n            launch {\n                offsetY.animateTo(bcy, currentAnimateSpec)\n            }\n        }\n    }\n\n    /**\n     * 放大或缩小\n     */\n    suspend fun toggleScale(\n        offset: Offset,\n        animationSpec: AnimationSpec<Float> = defaultAnimateSpec\n    ) {\n        // 如果不等于1，就调回1\n        if (scale.value != 1F) {\n            reset(animationSpec)\n        } else {\n            scaleToMax(offset, animationSpec)\n        }\n    }\n\n    /**\n     * 修正offsetX,offsetY的位置\n     */\n    suspend fun fixToBound() {\n        val boundX =\n            getBound(defaultSize.width.toFloat() * scale.value, containerSize.width.toFloat())\n        val boundY =\n            getBound(defaultSize.height.toFloat() * scale.value, containerSize.height.toFloat())\n        val limitX = limitToBound(offsetX.value, boundX)\n        val limitY = limitToBound(offsetY.value, boundY)\n        offsetX.snapTo(limitX)\n        offsetY.snapTo(limitY)\n    }\n\n    companion object {\n        val SAVER: Saver<ImageViewerState, *> = listSaver(save = {\n            listOf(it.offsetX.value, it.offsetY.value, it.scale.value, it.rotation.value)\n        }, restore = {\n            val state = ImageViewerState(\n                offsetX = it[0],\n                offsetY = it[1],\n                scale = it[2],\n                rotation = it[3],\n            )\n            state.fromSaver = true\n            state\n        })\n    }\n}\n\n/**\n * 记录viewer状态\n * @return ImageViewerState 返回一个状态实例\n */\n@Composable\nfun rememberViewerState(\n    // X轴偏移量\n    offsetX: Float = DEFAULT_OFFSET_X,\n    // Y轴偏移量\n    offsetY: Float = DEFAULT_OFFSET_Y,\n    // 缩放率\n    scale: Float = DEFAULT_SCALE,\n    // 旋转\n    rotation: Float = DEFAULT_ROTATION,\n    // 动画窗格\n    animationSpec: AnimationSpec<Float>? = null,\n    // 淡入淡出效果\n    crossfadeAnimationSpec: AnimationSpec<Float>? = null,\n): ImageViewerState = rememberSaveable(saver = ImageViewerState.SAVER) {\n    ImageViewerState(offsetX, offsetY, scale, rotation, animationSpec, crossfadeAnimationSpec)\n}\n\n/**\n * viewer手势对象\n */\nclass ViewerGestureScope(\n    // 点击事件\n    var onTap: (Offset) -> Unit = {},\n    // 双击事件\n    var onDoubleTap: (Offset) -> Unit = {},\n    // 长按事件\n    var onLongPress: (Offset) -> Unit = {},\n)\n\n/**\n * viewer传入的Compose数据类型参数\n * @property content [@androidx.compose.runtime.Composable] [@kotlin.ExtensionFunctionType] Function1<ComposeModelScope, Unit>\n * @constructor\n */\nclass ComposeModel(\n    private val content: @Composable ComposeModel.() -> Unit = {}\n) {\n\n    internal var intrinsicSize by mutableStateOf(IntSize.Zero)\n\n    fun updateIntrinsicSize(size: IntSize) {\n        intrinsicSize = size\n    }\n\n    @Composable\n    fun PoseContent() {\n        content()\n    }\n}\n\n/**\n * model支持Painter、ImageBitmap、ImageVector、ImageDecoder、ComposeModel\n */\n@Composable\nfun ImageViewer(\n    // 修改参数\n    modifier: Modifier = Modifier,\n    // 图片数据\n    model: Any?,\n    // viewer状态\n    state: ImageViewerState = rememberViewerState(),\n    // 检测手势\n    detectGesture: ViewerGestureScope.() -> Unit = {},\n    // 超出容器是否显示\n    boundClip: Boolean = true,\n    // 调试模式\n    debugMode: Boolean = false,\n) {\n    val viewerGestureScope = remember { ViewerGestureScope() }\n    detectGesture.invoke(viewerGestureScope)\n    val scope = rememberCoroutineScope()\n    // 触摸时中心位置\n    var centroid by remember { mutableStateOf(Offset.Zero) }\n    // 减速运动动画曲线\n    val decay = remember {\n        FloatExponentialDecaySpec(2f).generateDecayAnimationSpec<Float>()\n    }\n    var velocityTracker = remember { VelocityTracker() }\n    // 记录触摸事件中手指的个数\n    var eventChangeCount by remember { mutableStateOf(0) }\n    // 最后一次偏移运动\n    var lastPan by remember { mutableStateOf(Offset.Zero) }\n    // 手势实时的偏移范围\n    var boundX by remember { mutableStateOf(0F) }\n    var boundY by remember { mutableStateOf(0F) }\n    // 最大缩放率，双击的时候会放大到这个值\n    var maxScale by remember { mutableStateOf(1F) }\n    // 最大显示缩放率，缩放率超过这个值后，手势结束了就会自动恢复到这个值\n    val maxDisplayScale by remember { derivedStateOf { maxScale * MAX_SCALE_RATE } }\n    // 目标偏移量\n    var desX by remember { mutableStateOf(0F) }\n    var desY by remember { mutableStateOf(0F) }\n    // 目标缩放率\n    var desScale by remember { mutableStateOf(1F) }\n    // 缩放率修改前的值\n    var fromScale by remember { mutableStateOf(1F) }\n    // 计算边界使用的缩放率\n    var boundScale by remember { mutableStateOf(1F) }\n    // 目标旋转角度\n    var desRotation by remember { mutableStateOf(0F) }\n    // 要增加的旋转角度\n    var rotate by remember { mutableStateOf(0F) }\n    // 要增加的放大倍率\n    var zoom by remember { mutableStateOf(1F) }\n    // 两个手指的距离\n    var fingerDistanceOffset by remember { mutableStateOf(Offset.Zero) }\n\n    // 同步des的参数，在gallery的图片切换时，缩小后仍然接收手势指令，所以需要同步缩小后的参数\n    fun asyncDesParams() {\n        desX = state.offsetX.value\n        desY = state.offsetY.value\n        desScale = state.scale.value\n        desRotation = state.rotation.value\n    }\n    LaunchedEffect(key1 = state.resetTimeStamp) {\n        asyncDesParams()\n    }\n    val gesture = remember {\n        RawGesture(\n            onTap = viewerGestureScope.onTap,\n            onDoubleTap = viewerGestureScope.onDoubleTap,\n            onLongPress = viewerGestureScope.onLongPress,\n            gestureStart = {\n                if (state.allowGestureInput) {\n                    eventChangeCount = 0\n                    velocityTracker = VelocityTracker()\n                    scope.launch {\n                        state.offsetX.stop()\n                        state.offsetY.stop()\n                        state.offsetX.updateBounds(null, null)\n                        state.offsetY.updateBounds(null, null)\n                    }\n                    asyncDesParams()\n                }\n            },\n            gestureEnd = { transformOnly ->\n                // transformOnly记录手势事件中是否有位移，如果只是点击或双击，会返回false\n                // 如果正在动画中，就不要执行后续动作，如：reset指令执行时\n                if (transformOnly && !state.isRunning() && state.allowGestureInput) {\n                    // 处理加速度添加的点为空的情况\n                    var velocity = try {\n                        velocityTracker.calculateVelocity()\n                    } catch (e: Exception) {\n                        e.printStackTrace()\n                        null\n                    }\n                    // 如果缩放比小于1，要自动回到1\n                    // 如果缩放比大于最大显示缩放比，就设置回去，并且避免加速度\n                    val scale = when {\n                        state.scale.value < 1 -> 1F\n                        state.scale.value > maxDisplayScale -> {\n                            velocity = null\n                            maxDisplayScale\n                        }\n\n                        else -> null\n                    }\n                    // 如果此时位移超出范围，就动画回范围内\n                    // 如果没超出范围，就设置animate的范围，然后执行抛掷动画\n                    scope.launch {\n                        if (inBound(state.offsetX.value, boundX) && velocity != null) {\n                            val vx = sameDirection(lastPan.x, velocity.x)\n                            state.offsetX.updateBounds(-boundX, boundX)\n                            state.offsetX.animateDecay(vx, decay)\n                        } else {\n                            val targetX = if (scale != maxDisplayScale) {\n                                limitToBound(state.offsetX.value, boundX)\n                            } else {\n                                panTransformAndScale(\n                                    offset = state.offsetX.value,\n                                    center = centroid.x,\n                                    bh = state.containerSize.width.toFloat(),\n                                    uh = state.defaultSize.width.toFloat(),\n                                    fromScale = state.scale.value,\n                                    toScale = scale,\n                                )\n                            }\n                            state.offsetX.animateTo(targetX)\n                        }\n                    }\n                    scope.launch {\n                        if (inBound(state.offsetY.value, boundY) && velocity != null) {\n                            val vy = sameDirection(lastPan.y, velocity.y)\n                            state.offsetY.updateBounds(-boundY, boundY)\n                            state.offsetY.animateDecay(vy, decay)\n                        } else {\n                            val targetY = if (scale != maxDisplayScale) {\n                                limitToBound(state.offsetY.value, boundY)\n                            } else {\n                                panTransformAndScale(\n                                    offset = state.offsetY.value,\n                                    center = centroid.y,\n                                    bh = state.containerSize.height.toFloat(),\n                                    uh = state.defaultSize.height.toFloat(),\n                                    fromScale = state.scale.value,\n                                    toScale = scale,\n                                )\n                            }\n                            state.offsetY.animateTo(targetY)\n                        }\n                    }\n                    scope.launch {\n                        state.rotation.animateTo(0F)\n                    }\n                    scale?.let {\n                        scope.launch {\n                            state.scale.animateTo(scale)\n                        }\n                    }\n                }\n            },\n        ) { center, pan, _zoom, _rotate, event ->\n            // 当禁止手势输入时\n            if (!state.allowGestureInput) return@RawGesture true\n            // 这里只记录最大手指数\n            if (event.changes.size > eventChangeCount) eventChangeCount = event.changes.size\n            // 如果手指数从多个变成一个，就结束本次手势操作\n            if (eventChangeCount > event.changes.size) return@RawGesture false\n\n            rotate = _rotate\n            zoom = _zoom\n            // 如果是双指的情况下，手指距离小于一定值时，缩放和旋转的值会很离谱，所以在这种极端情况下就不要处理缩放和旋转了\n            if (event.changes.size == 2) {\n                fingerDistanceOffset = event.changes[0].position - event.changes[1].position\n                if (\n                    fingerDistanceOffset.x.absoluteValue < MIN_GESTURE_FINGER_DISTANCE\n                    && fingerDistanceOffset.y.absoluteValue < MIN_GESTURE_FINGER_DISTANCE\n                ) {\n                    rotate = 0F\n                    zoom = 1F\n                }\n            }\n            // 上一次的偏移量\n            lastPan = pan\n            // 记录手势的中点\n            centroid = center\n            // 记录当前缩放比\n            fromScale = desScale\n            // 目标放大倍率\n            desScale *= zoom\n            // 检查最小放大倍率\n            if (desScale < MIN_SCALE) desScale = MIN_SCALE\n\n            // 计算边界，如果目标缩放值超过最大显示缩放值，边界就要用最大缩放值来计算，否则手势结束时会导致无法归位\n            boundScale = if (desScale > maxDisplayScale) maxDisplayScale else desScale\n            boundX =\n                getBound(boundScale * state.defaultSize.width, state.containerSize.width.toFloat())\n            boundY =\n                getBound(\n                    boundScale * state.defaultSize.height,\n                    state.containerSize.height.toFloat()\n                )\n\n            desX = panTransformAndScale(\n                offset = desX,\n                center = center.x,\n                bh = state.containerSize.width.toFloat(),\n                uh = state.defaultSize.width.toFloat(),\n                fromScale = fromScale,\n                toScale = desScale,\n            ) + pan.x\n            // 如果手指数1，就是拖拽，拖拽受范围限制\n            // 如果手指数大于1，即有缩放事件，则支持中心点放大\n            if (eventChangeCount == 1) desX = limitToBound(desX, boundX)\n            desY = panTransformAndScale(\n                offset = desY,\n                center = center.y,\n                bh = state.containerSize.height.toFloat(),\n                uh = state.defaultSize.height.toFloat(),\n                fromScale = fromScale,\n                toScale = desScale,\n            ) + pan.y\n            if (eventChangeCount == 1) desY = limitToBound(desY, boundY)\n\n            if (desScale < 1) desRotation += rotate\n            velocityTracker.addPosition(\n                event.changes[0].uptimeMillis,\n                Offset(desX, desY),\n            )\n            if (!state.isRunning()) scope.launch {\n                state.scale.snapTo(desScale)\n                state.offsetX.snapTo(desX)\n                state.offsetY.snapTo(desY)\n                state.rotation.snapTo(desRotation)\n            }\n\n            // 这里判断是否已运动到边界，如果到了边界，就不消费事件，让上层界面获取到事件\n            val onLeft = desX >= boundX\n            val onRight = desX <= -boundX\n            val reachSide = !(onLeft && pan.x > 0)\n                    && !(onRight && pan.x < 0)\n                    && !(onLeft && onRight)\n            if (reachSide || state.scale.value < 1) {\n                event.changes.fastForEach {\n                    if (it.positionChanged()) {\n                        it.consumeAllChanges()\n                    }\n                }\n            }\n\n            // 返回true，继续下一次手势\n            return@RawGesture true\n        }\n    }\n    val sizeChange: suspend (SizeChangeContent) -> Unit = { content ->\n        maxScale = content.maxScale\n        state.defaultSize = content.defaultSize\n        state.containerSize = content.containerSize\n        state.maxScale = content.maxScale\n        if (state.fromSaver) {\n            state.fromSaver = false\n            state.fixToBound()\n        }\n    }\n    Box(modifier = modifier) {\n        /**\n         * 将挂载信息通知到state\n         */\n        val onMounted: () -> Unit = {\n            scope.launch {\n                state.mountedFlow.emit(true)\n            }\n        }\n        /**\n         * 根据不同类型的model进行不同的渲染\n         */\n        when (model) {\n            is Painter,\n            is ImageVector,\n            is ImageBitmap,\n            is ComposeModel,\n                -> {\n                ImageComposeOrigin(\n                    model = model,\n                    scale = state.scale.value,\n                    offsetX = state.offsetX.value,\n                    offsetY = state.offsetY.value,\n                    rotation = state.rotation.value,\n                    gesture = gesture,\n                    onSizeChange = sizeChange,\n                    onMounted = onMounted,\n                    boundClip = boundClip,\n                    crossfadeAnimationSpec = state.crossfadeAnimationSpec,\n                )\n            }\n\n            is ImageDecoder -> {\n                ImageComposeCanvas(\n                    imageDecoder = model,\n                    scale = state.scale.value,\n                    offsetX = state.offsetX.value,\n                    offsetY = state.offsetY.value,\n                    rotation = state.rotation.value,\n                    gesture = gesture,\n                    onSizeChange = sizeChange,\n                    onMounted = onMounted,\n                    boundClip = boundClip,\n                    crossfadeAnimationSpec = state.crossfadeAnimationSpec,\n                )\n            }\n        }\n\n        /**\n         * 调试模式\n         */\n        if (debugMode) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .zIndex(10F)\n            ) {\n                if (model != null) {\n                    Text(text = \"Model -> ON\", Modifier.background(Color.White))\n                }\n                Box(\n                    modifier = Modifier\n                        .graphicsLayer {\n                            translationX = centroid.x - 6.dp.toPx()\n                            translationY = centroid.y - 6.dp.toPx()\n                        }\n                        .clip(CircleShape)\n                        .background(Color.Red.copy(0.4f))\n                        .size(12.dp)\n                )\n            }\n        }\n    }\n}\n\n/**\n * 重写事件监听方法\n */\nsuspend fun PointerInputScope.detectTransformGestures(\n    panZoomLock: Boolean = false,\n    gestureStart: () -> Unit = {},\n    gestureEnd: (Boolean) -> Unit = {},\n    onTap: (Offset) -> Unit = {},\n    onDoubleTap: (Offset) -> Unit = {},\n    onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float, event: PointerEvent) -> Boolean,\n) {\n    var lastReleaseTime = 0L\n    var scope: CoroutineScope? = null\n    forEachGesture {\n        awaitPointerEventScope {\n            var rotation = 0f\n            var zoom = 1f\n            var pan = Offset.Zero\n            var pastTouchSlop = false\n            val touchSlop = viewConfiguration.touchSlop\n            var lockedToPanZoom = false\n\n            awaitFirstDown(requireUnconsumed = false)\n            val t0 = System.currentTimeMillis()\n            var releasedEvent: PointerEvent? = null\n            var moveCount = 0\n            // 这里开始事件\n            gestureStart()\n            do {\n                val event = awaitPointerEvent()\n                if (event.type == PointerEventType.Release) releasedEvent = event\n                if (event.type == PointerEventType.Move) moveCount++\n                val canceled = event.changes.fastAny { it.positionChangeConsumed() }\n                if (!canceled) {\n                    val zoomChange = event.calculateZoom()\n                    val rotationChange = event.calculateRotation()\n                    val panChange = event.calculatePan()\n\n                    if (!pastTouchSlop) {\n                        zoom *= zoomChange\n                        rotation += rotationChange\n                        pan += panChange\n\n                        val centroidSize = event.calculateCentroidSize(useCurrent = false)\n                        val zoomMotion = abs(1 - zoom) * centroidSize\n                        val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)\n                        val panMotion = pan.getDistance()\n\n                        if (zoomMotion > touchSlop ||\n                            rotationMotion > touchSlop ||\n                            panMotion > touchSlop\n                        ) {\n                            pastTouchSlop = true\n                            lockedToPanZoom = panZoomLock && rotationMotion < touchSlop\n                        }\n                    }\n                    if (pastTouchSlop) {\n                        val centroid = event.calculateCentroid(useCurrent = false)\n                        val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange\n                        if (effectiveRotation != 0f ||\n                            zoomChange != 1f ||\n                            panChange != Offset.Zero\n                        ) {\n                            if (!onGesture(\n                                    centroid,\n                                    panChange,\n                                    zoomChange,\n                                    effectiveRotation,\n                                    event\n                                )\n                            ) break\n                        }\n                    }\n                }\n            } while (!canceled && event.changes.fastAny { it.pressed })\n\n            var t1 = System.currentTimeMillis()\n            val dt = t1 - t0\n            val dlt = t1 - lastReleaseTime\n\n            if (moveCount == 0) releasedEvent?.let { e ->\n                if (e.changes.isEmpty()) return@let\n                val offset = e.changes.first().position\n                if (dlt < 272) {\n                    t1 = 0L\n                    scope?.cancel()\n                    onDoubleTap(offset)\n                } else if (dt < 200) {\n                    scope = MainScope()\n                    scope?.launch(Dispatchers.Main) {\n                        delay(272)\n                        onTap(offset)\n                    }\n                }\n                lastReleaseTime = t1\n            }\n\n            // 这里是事件结束\n            gestureEnd(moveCount != 0)\n        }\n    }\n}\n\n/**\n * 让后一个数与前一个数的符号保持一致\n * @param a Float\n * @param b Float\n * @return Float\n */\nfun sameDirection(a: Float, b: Float): Float {\n    return if (a > 0) {\n        if (b < 0) {\n            b.absoluteValue\n        } else {\n            b\n        }\n    } else {\n        if (b > 0) {\n            -b\n        } else {\n            b\n        }\n    }\n}\n\n/**\n * 获取移动边界\n */\nfun getBound(rw: Float, bw: Float): Float {\n    return if (rw > bw) {\n        var xb = (rw - bw).div(2)\n        if (xb < 0) xb = 0F\n        xb\n    } else {\n        0F\n    }\n}\n\n/**\n * 判断位移是否在边界内\n */\nfun inBound(offset: Float, bound: Float): Boolean {\n    return if (offset > 0) {\n        offset < bound\n    } else if (offset < 0) {\n        offset > -bound\n    } else {\n        true\n    }\n}\n\n/**\n * 把位移限制在边界内\n */\nfun limitToBound(offset: Float, bound: Float): Float {\n    return when {\n        offset > bound -> {\n            bound\n        }\n\n        offset < -bound -> {\n            -bound\n        }\n\n        else -> {\n            offset\n        }\n    }\n}\n\n/**\n * 追踪缩放过程中的中心点\n */\nfun panTransformAndScale(\n    offset: Float,\n    center: Float,\n    bh: Float,\n    uh: Float,\n    fromScale: Float,\n    toScale: Float,\n): Float {\n    val srcH = uh * fromScale\n    val desH = uh * toScale\n    val gapH = (bh - uh) / 2\n\n    val py = when {\n        uh >= bh -> {\n            val upy = (uh * fromScale - uh).div(2)\n            (upy - offset + center) / (fromScale * uh)\n        }\n\n        srcH > bh || bh > uh -> {\n            val upy = (srcH - uh).div(2)\n            (upy - gapH - offset + center) / (fromScale * uh)\n        }\n\n        else -> {\n            val upy = -(bh - srcH).div(2)\n            (upy - offset + center) / (fromScale * uh)\n        }\n    }\n    return when {\n        uh >= bh -> {\n            val upy = (uh * toScale - uh).div(2)\n            upy + center - py * toScale * uh\n        }\n\n        desH > bh -> {\n            val upy = (desH - uh).div(2)\n            upy - gapH + center - py * toScale * uh\n        }\n\n        else -> {\n            val upy = -(bh - desH).div(2)\n            upy + center - py * desH\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/DynamicDetailActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.mobile.screen.DynamicDetailScreen\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\nclass DynamicDetailActivity : ComponentActivity() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n\n        fun actionStart(context: Context, dynamicId: String) {\n            logger.info { \"actionStart: dynamicId=$dynamicId\" }\n            context.startActivity(\n                Intent(context, DynamicDetailActivity::class.java).apply {\n                    putExtra(\"dynamicId\", dynamicId)\n                }\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVMobileTheme {\n                DynamicDetailScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/FavoriteActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport dev.aaa1115910.bv.mobile.screen.FavoriteScreen\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\nclass FavoriteActivity : ComponentActivity() {\n    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            val windowSize = calculateWindowSizeClass(this)\n            BVMobileTheme {\n                FavoriteScreen(\n                    windowSize = windowSize\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/FollowingSeasonActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport dev.aaa1115910.bv.mobile.screen.FollowingSeasonScreen\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\nclass FollowingSeasonActivity : ComponentActivity() {\n    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            val windowSize = calculateWindowSizeClass(this)\n            BVMobileTheme {\n                FollowingSeasonScreen(\n                    windowSize = windowSize\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/FollowingUserActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.mobile.screen.FollowingUserScreen\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\nclass FollowingUserActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVMobileTheme {\n                FollowingUserScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/HistoryActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport dev.aaa1115910.bv.mobile.screen.HistoryScreen\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\nclass HistoryActivity : ComponentActivity() {\n    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            val windowSize = calculateWindowSizeClass(this)\n            BVMobileTheme {\n                HistoryScreen(\n                    windowSize = windowSize\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/IntentHandlerActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport dev.aaa1115910.bv.entity.BvScheme\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\nclass IntentHandlerActivity : ComponentActivity() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val uri = intent.data\n        when (uri?.host) {\n            BvScheme.QrToken.HOST -> QrTokenResultActivity.launch(this, uri)\n            else -> {\n                logger.info { \"unknown uri: $uri\" }\n                finish()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/LoginActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.mobile.screen.LoginScreen\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\nclass LoginActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVMobileTheme {\n                LoginScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/MainActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen\nimport dev.aaa1115910.bv.mobile.screen.MobileMainScreen\nimport dev.aaa1115910.bv.mobile.screen.RegionBlockScreen\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.NetworkUtil\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\nclass MainActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        var keepSplashScreen = true\n        installSplashScreen().apply {\n            setKeepOnScreenCondition { keepSplashScreen }\n        }\n        super.onCreate(savedInstanceState)\n\n        setContent {\n            val scope = rememberCoroutineScope()\n            var isCheckingNetwork by remember { mutableStateOf(true) }\n            var isMainlandChina by remember { mutableStateOf(false) }\n\n            LaunchedEffect(Unit) {\n                scope.launch(Dispatchers.IO) {\n                    isMainlandChina = false // NetworkUtil.isMainlandChina()\n                    isCheckingNetwork = false\n                    keepSplashScreen = false\n                }\n            }\n\n            BVMobileTheme {\n                if (isCheckingNetwork) {\n                    // 避免提前加载内容\n//                } else if (isMainlandChina) {\n//                    RegionBlockScreen()\n                } else {\n                    MobileMainScreen()\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/QrTokenResultActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.mobile.screen.QrTokenResultScreen\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\nclass QrTokenResultActivity : ComponentActivity() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n\n        fun launch(context: Context, uri: Uri) {\n            logger.info { \"launch QrTokenResultActivity: uri=$uri\" }\n            context.startActivity(\n                Intent(context, QrTokenResultActivity::class.java).apply {\n                    putExtra(\"uri\", uri)\n                }\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        setContent {\n            QrTokenResultScreen()\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/SettingsActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.mobile.screen.settings.SettingsScreen\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\nclass SettingsActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVMobileTheme {\n                SettingsScreen()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/UserSpaceActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.mobile.screen.UserSpaceScreen\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\nclass UserSpaceActivity : ComponentActivity() {\n    companion object {\n        fun actionStart(context: Context, mid: Long, name: String) {\n            context.startActivity(\n                Intent(context, UserSpaceActivity::class.java).apply {\n                    putExtra(\"mid\", mid)\n                    putExtra(\"name\", name)\n                }\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVMobileTheme {\n                UserSpaceScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/activities/VideoPlayerActivity.kt",
    "content": "package dev.aaa1115910.bv.mobile.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport androidx.lifecycle.lifecycleScope\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.PlayerType\nimport dev.aaa1115910.bv.mobile.screen.VideoPlayerScreen\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.player.VideoPlayerOptions\nimport dev.aaa1115910.bv.player.impl.exo.ExoPlayerFactory\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.CommentViewModel\nimport dev.aaa1115910.bv.viewmodel.VideoPlayerV3ViewModel\nimport dev.aaa1115910.bv.viewmodel.video.VideoDetailViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass VideoPlayerActivity : ComponentActivity() {\n    companion object {\n        fun actionStart(\n            context: Context,\n            aid: Long,\n            //cid: Long,\n            fromSeason: Boolean = false,\n            epid: Int? = null,\n            seasonId: Int? = null,\n        ) {\n            context.startActivity(\n                Intent(context, VideoPlayerActivity::class.java).apply {\n                    putExtra(\"aid\", aid)\n                    //putExtra(\"cid\", cid)\n                    putExtra(\"fromSeason\", fromSeason)\n                    epid?.let { putExtra(\"epid\", it) }\n                    seasonId?.let { putExtra(\"seasonId\", it) }\n                }\n            )\n        }\n    }\n\n    private val playerViewModel: VideoPlayerV3ViewModel by viewModel()\n    private val commentViewModel: CommentViewModel by viewModel()\n    private val videoDetailViewModel: VideoDetailViewModel by viewModel()\n    private val logger = KotlinLogging.logger {}\n\n    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        initVideoPlayer()\n        setContent {\n            val windowSizeClass = calculateWindowSizeClass(this)\n            BVMobileTheme {\n                VideoPlayerScreen(\n                    windowSizeClass = windowSizeClass\n                )\n            }\n        }\n    }\n\n    private fun initVideoPlayer() {\n        if (playerViewModel.videoPlayer != null) return\n        logger.fInfo { \"initVideoPlayer\" }\n        val options = VideoPlayerOptions(\n            userAgent = when (Prefs.apiType) {\n                ApiType.Web -> dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_WEB\n                ApiType.App -> dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_APP\n            },\n            referer = when (Prefs.apiType) {\n                ApiType.Web -> getString(R.string.video_player_referer)\n                ApiType.App -> null\n            }\n        )\n        val videoPlayer = when (Prefs.playerType) {\n            PlayerType.Media3 -> ExoPlayerFactory().create(this, options)\n        }\n        playerViewModel.videoPlayer = videoPlayer\n        //TODO 还没处理旋转后的一些判断，就先放这了\n        parseIntent()\n    }\n\n    private fun parseIntent() {\n        var aid = intent.getLongExtra(\"aid\", 0)\n        var cid = intent.getLongExtra(\"cid\", 0)\n        val fromSeason = intent.getBooleanExtra(\"fromSeason\", false)\n        val epid = intent.getIntExtra(\"epid\", 0)\n        val seasonId = intent.getIntExtra(\"seasonId\", 0)\n\n        lifecycleScope.launch(Dispatchers.IO) {\n            if (aid == 0L && cid == 0L) {\n                runCatching {\n                    val acid = BiliHttpApi.getAidCidByEpid(epid)!!\n                    aid = acid.first\n                    cid = acid.second\n                }.onFailure {\n                    logger.fInfo { \"get avid & cid by epid failed: ${it.stackTraceToString()}\" }\n                    withContext(Dispatchers.Main) {\n                        it.message?.toast(this@VideoPlayerActivity)\n                    }\n                }\n            }\n\n            commentViewModel.commentType = 1\n            commentViewModel.commentId = aid\n\n            runCatching {\n                videoDetailViewModel.loadDetail(aid, fromSeason)\n            }.onFailure {\n                withContext(Dispatchers.Main) {\n                    it.message?.toast(this@VideoPlayerActivity)\n                }\n            }\n            runCatching {\n                playerViewModel.fromSeason = fromSeason\n                playerViewModel.loadPlayUrl(\n                    avid = videoDetailViewModel.videoDetail?.aid ?: 0,\n                    cid = videoDetailViewModel.videoDetail?.cid ?: 0,\n                    epid = epid.takeIf { it != 0 },\n                    seasonId = seasonId.takeIf { it != 0 }\n                )\n            }.onFailure {\n                withContext(Dispatchers.Main) {\n                    it.message?.toast(this@VideoPlayerActivity)\n                }\n            }\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        if (isFinishing) {\n            playerViewModel.releasePlayerResources(\"onDestroy\")\n        }\n    }\n\n    override fun onPause() {\n        playerViewModel.videoPlayer?.isInBackground = true\n        playerViewModel.videoPlayer?.pause()\n        super.onPause()\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/home/SearchBar.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.home\n\nimport android.content.Context\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Menu\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material.icons.filled.Person\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material.icons.filled.Star\nimport androidx.compose.material3.DockedSearchBar\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SearchBar\nimport androidx.compose.material3.SearchBarDefaults\nimport androidx.compose.material3.Tab\nimport androidx.compose.material3.TabRow\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.semantics.isTraversalGroup\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.traversalIndex\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.zIndex\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@Composable\nfun HomeSearchTopBarCompact(\n    modifier: Modifier = Modifier,\n    query: String,\n    expanded: Boolean,\n    selectedTabIndex: Int,\n    onQueryChange: (String) -> Unit,\n    onExpandedChange: (Boolean) -> Unit,\n    onOpenNavDrawer: () -> Unit,\n    onChangeTabIndex: (Int) -> Unit,\n    onSwitchUser: () -> Unit\n) {\n    val context = LocalContext.current\n    var currentTab by remember { mutableStateOf(HomeTab.Recommend) }\n\n    val searchBarHorizontalPadding by animateDpAsState(\n        targetValue = if (expanded) 0.dp else 16.dp,\n        label = \"search bar horizontal padding\"\n    )\n\n    Box(\n        modifier = modifier,\n        contentAlignment = Alignment.TopCenter\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = searchBarHorizontalPadding)\n                .zIndex(2f),\n            horizontalArrangement = Arrangement.Center\n        ) {\n            HomeSearchBar(\n                modifier = Modifier.fillMaxWidth(),\n                query = query,\n                expanded = expanded,\n                onQueryChange = onQueryChange,\n                onExpandedChange = onExpandedChange,\n                onOpenNavDrawer = onOpenNavDrawer,\n                onSwitchUser = onSwitchUser,\n                onSearch = {}\n            )\n        }\n\n        TabRow(\n            modifier = Modifier\n                .padding(top = 100.dp)\n                .zIndex(1f),\n            selectedTabIndex = selectedTabIndex\n        ) {\n            HomeTab.entries.forEachIndexed { index, tab ->\n                Tab(\n                    selected = selectedTabIndex == index,\n                    onClick = {\n                        onChangeTabIndex(index)\n                        currentTab = tab\n                    },\n                    text = {\n                        Text(\n                            text = tab.getDisplayName(context),\n                            maxLines = 2,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun HomeSearchTopBarExpanded(\n    modifier: Modifier = Modifier,\n    query: String,\n    active: Boolean,\n    onQueryChange: (String) -> Unit,\n    onActiveChange: (Boolean) -> Unit,\n) {\n    Box(\n        modifier\n            .semantics { isTraversalGroup = true }\n            .zIndex(1f)\n            .fillMaxWidth()\n    ) {\n        Spacer(\n            modifier = Modifier\n                .fillMaxWidth()\n                .height(60.dp)\n                .background(MaterialTheme.colorScheme.surface)\n        )\n        Row(\n            modifier = Modifier,\n            verticalAlignment = Alignment.Top\n        ) {\n            DockedSearchBar(\n                modifier = Modifier.padding(vertical = 3.dp, horizontal = 16.dp),\n                inputField = {\n                    SearchBarDefaults.InputField(\n                        query = query,\n                        onQueryChange = onQueryChange,\n                        onSearch = {},\n                        expanded = active,\n                        onExpandedChange = onActiveChange\n                    )\n                },\n                expanded = active,\n                onExpandedChange = onActiveChange\n            ) {\n                Text(\"???\")\n            }\n\n            val titles = listOf(\"Tab 1\", \"Tab 2\", \"Tab 3 with lots of text\")\n            var state by remember { mutableStateOf(0) }\n\n            Box(\n                modifier = Modifier\n                    .height(62.dp)\n                    .fillMaxWidth(),\n                contentAlignment = Alignment.BottomCenter\n            ) {\n                TabRow(\n                    selectedTabIndex = state\n                ) {\n                    titles.forEachIndexed { index, title ->\n                        Tab(\n                            selected = state == index,\n                            onClick = { state = index },\n                            text = {\n                                Text(\n                                    text = title,\n                                    maxLines = 2,\n                                    overflow = TextOverflow.Ellipsis\n                                )\n                            }\n                        )\n                    }\n                }\n            }\n\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun HomeSearchBar(\n    modifier: Modifier = Modifier,\n    query: String,\n    expanded: Boolean,\n    onQueryChange: (String) -> Unit,\n    onExpandedChange: (Boolean) -> Unit,\n    onOpenNavDrawer: () -> Unit,\n    onSwitchUser: () -> Unit,\n    onSearch: (String) -> Unit,\n) {\n    SearchBar(\n        modifier = modifier,\n        inputField = {\n            SearchBarDefaults.InputField(\n                query = query,\n                onQueryChange = onQueryChange,\n                onSearch = {\n                    onSearch(it)\n                    onExpandedChange(false)\n                },\n                expanded = expanded,\n                onExpandedChange = onExpandedChange,\n                leadingIcon = {\n                    if (!expanded) {\n                        IconButton(onClick = onOpenNavDrawer) {\n                            Icon(imageVector = Icons.Default.Menu, contentDescription = null)\n                        }\n                    } else {\n                        Icon(imageVector = Icons.Default.Search, contentDescription = null)\n                    }\n                },\n                trailingIcon = {\n                    if (!expanded) {\n                        IconButton(onClick = onSwitchUser) {\n                            Icon(imageVector = Icons.Default.Person, contentDescription = null)\n                        }\n                    }\n                },\n            )\n        },\n        expanded = expanded,\n        onExpandedChange = onExpandedChange,\n    ) { }\n}\n\nenum class HomeTab(private val strRes: Int) {\n    Recommend(R.string.home_tab_rcmd),\n    Popular(R.string.home_tab_popular);\n\n    fun getDisplayName(context: Context = BVApp.context) = context.getString(strRes)\n}\n\n@Preview\n@Composable\nprivate fun HomeSearchTopBarCompactPreview() {\n    var query by remember { mutableStateOf(\"\") }\n    val active by remember { derivedStateOf { query != \"\" } }\n\n    BVMobileTheme {\n        Column {\n            HomeSearchTopBarCompact(\n                query = query,\n                expanded = false,\n                selectedTabIndex = 0,\n                onQueryChange = { query = it },\n                onExpandedChange = { },\n                onOpenNavDrawer = { },\n                onChangeTabIndex = { },\n                onSwitchUser = { }\n            )\n\n            Text(text = \"query: $query\")\n            Text(text = \"active: $active\")\n\n        }\n\n    }\n}\n\n@Preview(device = \"spec:parent=pixel_5,orientation=landscape\")\n@Composable\nprivate fun HomeSearchTopBarExpandedPreview() {\n    BVMobileTheme {\n        HomeSearchTopBarExpanded(\n            query = \"Search\",\n            active = false,\n            onQueryChange = { },\n            onActiveChange = { },\n        )\n    }\n}\n\n@Preview\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun Demo(modifier: Modifier = Modifier) {\n    var text by rememberSaveable { mutableStateOf(\"\") }\n    var expanded by rememberSaveable { mutableStateOf(false) }\n    Box(\n        Modifier\n            .fillMaxSize()\n            .semantics { isTraversalGroup = true }\n    ) {\n        SearchBar(\n            modifier = Modifier\n                .align(Alignment.TopCenter)\n                .semantics { traversalIndex = 0f },\n            inputField = {\n                SearchBarDefaults.InputField(\n                    query = text,\n                    onQueryChange = { text = it },\n                    onSearch = { expanded = false },\n                    expanded = expanded,\n                    onExpandedChange = { expanded = it },\n                    placeholder = { Text(\"Hinted search text\") },\n                    leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },\n                    trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) },\n                )\n            },\n            expanded = expanded,\n            onExpandedChange = { expanded = it },\n        ) {\n            Column(\n                Modifier\n                    .verticalScroll(rememberScrollState())\n                    .imePadding()\n            ) {\n                repeat(40) { idx ->\n                    val resultText = \"Suggestion $idx\"\n                    ListItem(\n                        headlineContent = { Text(resultText) },\n                        supportingContent = { Text(\"Additional info\") },\n                        leadingContent = { Icon(Icons.Filled.Star, contentDescription = null) },\n                        colors = ListItemDefaults.colors(containerColor = Color.Transparent),\n                        modifier =\n                        Modifier\n                            .clickable {\n                                text = resultText\n                                expanded = false\n                            }\n                            .fillMaxWidth()\n                            .padding(horizontal = 16.dp, vertical = 4.dp)\n                    )\n                }\n            }\n        }\n        LazyColumn(\n            contentPadding = PaddingValues(start = 16.dp, top = 72.dp, end = 16.dp, bottom = 16.dp),\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n            modifier = Modifier.semantics { traversalIndex = 1f },\n        ) {\n            val list = List(100) { \"Text $it\" }\n            items(count = list.size) {\n                Text(\n                    text = list[it],\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp),\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/home/UserDialog.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.home\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowDropDown\nimport androidx.compose.material.icons.filled.ArrowDropUp\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.outlined.PersonAdd\nimport androidx.compose.material.icons.outlined.PersonRemove\nimport androidx.compose.material.icons.rounded.Favorite\nimport androidx.compose.material.icons.rounded.History\nimport androidx.compose.material.icons.rounded.Settings\nimport androidx.compose.material.icons.rounded.SupervisorAccount\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.CenterAlignedTopAppBar\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowSizeClass\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.entity.db.UserDB\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.ifElse\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Composable\nfun UserDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    windowWidthSizeClass: WindowWidthSizeClass = WindowWidthSizeClass.Compact,\n    currentUser: UserDB?,\n    userList: List<UserDB>,\n    onHideDialog: () -> Unit,\n    onSwitchUser: (UserDB) -> Unit,\n    onAddUser: () -> Unit,\n    onDeleteUser: (UserDB) -> Unit,\n    onOpenFollowingUser: () -> Unit,\n    onOpenHistory: () -> Unit,\n    onOpenFavorite: () -> Unit,\n    onOpenFollowingPgc: () -> Unit,\n    onOpenToView: () -> Unit,\n    onOpenSettings: () -> Unit,\n) {\n    if (show) {\n        Box(\n            modifier = modifier\n                .fillMaxSize()\n                .background(Color.Black.copy(alpha = 0.4f))\n                .clickable(\n                    interactionSource = null,\n                    indication = null,\n                    onClick = onHideDialog\n                )\n        ) {\n            UserDialogContent(\n                modifier = Modifier\n                    .padding(horizontal = 16.dp, vertical = 80.dp)\n                    .ifElse(\n                        { windowWidthSizeClass != WindowWidthSizeClass.Compact },\n                        Modifier.width(500.dp)\n                    )\n                    .clip(MaterialTheme.shapes.extraLarge)\n                    .align(Alignment.TopCenter),\n                currentUser = currentUser,\n                userList = userList,\n                onClose = onHideDialog,\n                onSwitchUser = onSwitchUser,\n                onAddUser = onAddUser,\n                onDeleteUser = onDeleteUser,\n                onOpenFollowingUser = onOpenFollowingUser,\n                onOpenHistory = onOpenHistory,\n                onOpenFavorite = onOpenFavorite,\n                onOpenFollowingPgc = onOpenFollowingPgc,\n                onOpenToView = onOpenToView,\n                onOpenSettings = onOpenSettings\n            )\n        }\n\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun UserDialogContent(\n    modifier: Modifier = Modifier,\n    currentUser: UserDB?,\n    userList: List<UserDB>,\n    onClose: () -> Unit,\n    onSwitchUser: (UserDB) -> Unit,\n    onAddUser: () -> Unit,\n    onDeleteUser: (UserDB) -> Unit,\n    onOpenFollowingUser: () -> Unit,\n    onOpenHistory: () -> Unit,\n    onOpenFavorite: () -> Unit,\n    onOpenFollowingPgc: () -> Unit,\n    onOpenToView: () -> Unit,\n    onOpenSettings: () -> Unit,\n) {\n    var expandUserManager by remember { mutableStateOf(false) }\n\n    Box(\n        modifier = modifier\n            .background(MaterialTheme.colorScheme.surfaceVariant)\n    ) {\n        Column {\n            CenterAlignedTopAppBar(\n                title = { Text(text = \"Bug Video\") },\n                navigationIcon = {\n                    IconButton(onClick = onClose) {\n                        Icon(imageVector = Icons.Default.Close, contentDescription = null)\n                    }\n                },\n                colors = TopAppBarDefaults.centerAlignedTopAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceVariant\n                ),\n                windowInsets = WindowInsets(0, 0, 0, 0),\n            )\n            Column(\n                modifier = Modifier\n                    .padding(horizontal = 16.dp)\n                    .verticalScroll(rememberScrollState()),\n                verticalArrangement = Arrangement.spacedBy(2.dp)\n            ) {\n                if (currentUser == null) {\n                    Card(\n                        colors = CardDefaults.cardColors(\n                            containerColor = MaterialTheme.colorScheme.surface\n                        ),\n                        shape = MaterialTheme.shapes.extraLarge\n                    ) {\n                        ListItem(\n                            modifier = Modifier.clickable { onAddUser() },\n                            headlineContent = { Text(text = \"登录\") },\n                            leadingContent = {\n                                Icon(\n                                    imageVector = Icons.Outlined.PersonAdd,\n                                    contentDescription = null\n                                )\n                            }\n                        )\n                    }\n                } else {\n                    Column {\n                        Card(\n                            colors = CardDefaults.cardColors(\n                                containerColor = MaterialTheme.colorScheme.surface\n                            ),\n                            shape = RoundedCornerShape(\n                                topStart = MaterialTheme.shapes.extraLarge.topStart,\n                                topEnd = MaterialTheme.shapes.extraLarge.topEnd,\n                                bottomStart = MaterialTheme.shapes.extraSmall.bottomStart,\n                                bottomEnd = MaterialTheme.shapes.extraSmall.bottomEnd\n                            )\n                        ) {\n                            UserItem(\n                                username = currentUser.username,\n                                avatar = currentUser.avatar,\n                                uid = currentUser.uid,\n                                expandUserManager = expandUserManager,\n                                onClick = {},\n                                onExpandUserManagerChange = { expandUserManager = it }\n                            )\n                        }\n                        AnimatedVisibility(\n                            visible = expandUserManager,\n                            enter = expandVertically(),\n                            exit = shrinkVertically()\n                        ) {\n                            Card(\n                                modifier = Modifier.padding(top = 2.dp),\n                                colors = CardDefaults.cardColors(\n                                    containerColor = MaterialTheme.colorScheme.surface\n                                ),\n                                shape = MaterialTheme.shapes.extraSmall\n                            ) {\n                                Column {\n                                    userList\n                                        .filter { it != currentUser }\n                                        .forEach { user ->\n                                            UserItem(\n                                                username = user.username,\n                                                avatar = user.avatar,\n                                                uid = user.uid,\n                                                onClick = {\n                                                    onSwitchUser(user)\n                                                    onClose()\n                                                }\n                                            )\n                                        }\n                                }\n                                ListItem(\n                                    modifier = Modifier.clickable { onAddUser() },\n                                    headlineContent = { Text(text = \"添加其他账号\") },\n                                    leadingContent = {\n                                        Icon(\n                                            modifier = Modifier\n                                                .width(40.dp)\n                                                .scale(scaleX = -1f, scaleY = 1f),\n                                            imageVector = Icons.Outlined.PersonAdd,\n                                            contentDescription = null\n                                        )\n                                    }\n                                )\n                                ListItem(\n                                    modifier = Modifier.clickable { onDeleteUser(currentUser) },\n                                    headlineContent = { Text(text = \"移除此设备上的账号\") },\n                                    leadingContent = {\n                                        Icon(\n                                            modifier = Modifier\n                                                .width(40.dp),\n                                            imageVector = Icons.Outlined.PersonRemove,\n                                            contentDescription = null\n                                        )\n                                    }\n                                )\n                            }\n                        }\n                    }\n\n                    Card(\n                        colors = CardDefaults.cardColors(\n                            containerColor = MaterialTheme.colorScheme.surface\n                        ),\n                        shape = RoundedCornerShape(\n                            topStart = MaterialTheme.shapes.extraSmall.topStart,\n                            topEnd = MaterialTheme.shapes.extraSmall.topEnd,\n                            bottomStart = MaterialTheme.shapes.extraLarge.bottomStart,\n                            bottomEnd = MaterialTheme.shapes.extraLarge.bottomEnd\n                        )\n                    ) {\n                        Column {\n                            ListItem(\n                                modifier = Modifier.clickable { onOpenFollowingUser() },\n                                headlineContent = { Text(text = \"我的关注\") },\n                                leadingContent = {\n                                    Icon(\n                                        imageVector = Icons.Rounded.SupervisorAccount,\n                                        contentDescription = null\n                                    )\n                                }\n                            )\n                            ListItem(\n                                modifier = Modifier.clickable { onOpenHistory() },\n                                headlineContent = { Text(text = \"历史记录\") },\n                                leadingContent = {\n                                    Icon(\n                                        imageVector = Icons.Rounded.History,\n                                        contentDescription = null\n                                    )\n                                }\n                            )\n                            ListItem(\n                                modifier = Modifier.clickable { onOpenFavorite() },\n                                headlineContent = { Text(text = \"我的收藏\") },\n                                leadingContent = {\n                                    Icon(\n                                        imageVector = Icons.Rounded.Favorite,\n                                        contentDescription = null\n                                    )\n                                }\n                            )\n                            ListItem(\n                                modifier = Modifier.clickable { onOpenFollowingPgc() },\n                                headlineContent = { Text(text = \"我的追番\") },\n                                leadingContent = {\n                                    Icon(\n                                        imageVector = Icons.Rounded.SupervisorAccount,\n                                        contentDescription = null\n                                    )\n                                }\n                            )\n                            ListItem(\n                                modifier = Modifier.clickable { onOpenToView() },\n                                headlineContent = { Text(text = \"稍后再看\") },\n                                leadingContent = {\n                                    Icon(\n                                        imageVector = Icons.Rounded.SupervisorAccount,\n                                        contentDescription = null\n                                    )\n                                }\n                            )\n                        }\n                    }\n                }\n\n                ListItem(\n                    modifier = Modifier.clickable { onOpenSettings() },\n                    headlineContent = { Text(text = \"设置\") },\n                    leadingContent = {\n                        Icon(\n                            imageVector = Icons.Rounded.Settings,\n                            contentDescription = null\n                        )\n                    },\n                    colors = ListItemDefaults.colors(\n                        containerColor = Color.Transparent\n                    )\n                )\n                Spacer(modifier = Modifier.height(16.dp))\n            }\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun UserDialogContentPreview() {\n    var currentUser by remember { mutableStateOf(UserDB(-1, -1, \"\", \"\", \"\")) }\n    val userList = remember { mutableStateListOf<UserDB>() }\n\n    LaunchedEffect(Unit) {\n        for (i in 0..5) {\n            userList.add(\n                UserDB(\n                    id = i,\n                    uid = 100000L + i,\n                    username = \"User $i\",\n                    avatar = \"\",\n                    auth = \"\"\n                )\n            )\n        }\n        currentUser = userList[1]\n    }\n\n    BVMobileTheme {\n        UserDialogContent(\n            currentUser = currentUser,\n            userList = userList,\n            onClose = {},\n            onSwitchUser = {},\n            onAddUser = {},\n            onDeleteUser = {},\n            onOpenFollowingUser = {},\n            onOpenHistory = {},\n            onOpenFavorite = {},\n            onOpenFollowingPgc = {},\n            onOpenToView = {},\n            onOpenSettings = {}\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun UserDialogContentLoginRequirePreview() {\n    var currentUser by remember { mutableStateOf<UserDB?>(null) }\n    val userList = remember { mutableStateListOf<UserDB>() }\n\n    BVMobileTheme {\n        UserDialogContent(\n            currentUser = currentUser,\n            userList = userList,\n            onClose = {},\n            onSwitchUser = {},\n            onAddUser = {},\n            onDeleteUser = {},\n            onOpenFollowingUser = {},\n            onOpenHistory = {},\n            onOpenFavorite = {},\n            onOpenFollowingPgc = {},\n            onOpenToView = {},\n            onOpenSettings = {}\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Preview\n@Composable\nprivate fun UserDialogPreview() {\n    var currentUser by remember { mutableStateOf(UserDB(-1, -1, \"\", \"\", \"\")) }\n    val userList = remember { mutableStateListOf<UserDB>() }\n\n    LaunchedEffect(Unit) {\n        for (i in 0..5) {\n            userList.add(\n                UserDB(\n                    id = i,\n                    uid = 100000L + i,\n                    username = \"User $i\",\n                    avatar = \"\",\n                    auth = \"\"\n                )\n            )\n        }\n        currentUser = userList[1]\n    }\n\n    BVMobileTheme {\n        UserDialog(\n            show = true,\n            currentUser = currentUser,\n            userList = userList,\n            onHideDialog = {},\n            onSwitchUser = {},\n            onAddUser = {},\n            onDeleteUser = {},\n            onOpenFollowingUser = {},\n            onOpenHistory = {},\n            onOpenFavorite = {},\n            onOpenFollowingPgc = {},\n            onOpenToView = {},\n            onOpenSettings = {}\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Preview(device = \"spec:width=1280dp,height=800dp,dpi=240\")\n@Composable\nprivate fun UserDialogWidthScreenPreview() {\n    val windowSize = WindowSizeClass.calculateFromSize(DpSize(1280.dp, 800.dp))\n    var currentUser by remember { mutableStateOf(UserDB(-1, -1, \"\", \"\", \"\")) }\n    val userList = remember { mutableStateListOf<UserDB>() }\n\n    LaunchedEffect(Unit) {\n        for (i in 0..5) {\n            userList.add(\n                UserDB(\n                    id = i,\n                    uid = 100000L + i,\n                    username = \"User $i\",\n                    avatar = \"\",\n                    auth = \"\"\n                )\n            )\n        }\n        currentUser = userList[1]\n    }\n\n    BVMobileTheme {\n        UserDialog(\n            show = true,\n            windowWidthSizeClass = windowSize.widthSizeClass,\n            currentUser = currentUser,\n            userList = userList,\n            onHideDialog = {},\n            onSwitchUser = {},\n            onAddUser = {},\n            onDeleteUser = {},\n            onOpenFollowingUser = {},\n            onOpenHistory = {},\n            onOpenFavorite = {},\n            onOpenFollowingPgc = {},\n            onOpenToView = {},\n            onOpenSettings = {}\n        )\n    }\n}\n\n\n@Composable\nprivate fun UserItem(\n    modifier: Modifier = Modifier,\n    username: String,\n    avatar: String,\n    uid: Long,\n    expandUserManager: Boolean = false,\n    onClick: () -> Unit,\n    onExpandUserManagerChange: ((Boolean) -> Unit)? = null\n) {\n    ListItem(\n        modifier = modifier\n            .clickable { onClick() },\n        headlineContent = {\n            Text(text = username)\n        },\n        supportingContent = {\n            Text(text = \"$uid\")\n        },\n        leadingContent = {\n            AsyncImage(\n                modifier = Modifier\n                    .size(40.dp)\n                    .clip(CircleShape)\n                    .background(Color.Gray),\n                model = avatar,\n                contentDescription = null,\n                contentScale = ContentScale.FillBounds\n            )\n        },\n        trailingContent = (@Composable {\n            if (expandUserManager) {\n                IconButton(onClick = { onExpandUserManagerChange?.invoke(false) }) {\n                    Icon(imageVector = Icons.Default.ArrowDropUp, contentDescription = null)\n                }\n            } else {\n                IconButton(onClick = { onExpandUserManagerChange?.invoke(true) }) {\n                    Icon(imageVector = Icons.Default.ArrowDropDown, contentDescription = null)\n                }\n            }\n        }).takeIf { onExpandUserManagerChange != null }\n    )\n}\n\n@Preview\n@Composable\nprivate fun UserItemPreview() {\n    BVMobileTheme {\n        UserItem(\n            username = \"username\",\n            avatar = \"\",\n            uid = 123456,\n            onClick = {}\n        )\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/home/dynamic/DynamicItem.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.home.dynamic\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.CornerSize\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.Comment\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.FavoriteBorder\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material.icons.filled.Share\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.tooling.preview.PreviewParameter\nimport androidx.compose.ui.tooling.preview.PreviewParameterProvider\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport coil.compose.AsyncImage\nimport coil.compose.rememberAsyncImagePainter\nimport com.origeek.imageViewer.previewer.ImagePreviewerState\nimport com.origeek.imageViewer.previewer.TransformImageView\nimport com.origeek.imageViewer.previewer.TransformItemState\nimport com.origeek.imageViewer.previewer.rememberPreviewerState\nimport com.origeek.imageViewer.previewer.rememberTransformItemState\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.biliapi.entity.user.DynamicItem\nimport dev.aaa1115910.biliapi.entity.user.DynamicType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.mobile.component.user.UserAvatar\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.notYetImplemented\nimport dev.aaa1115910.bv.util.resizedImageUrl\nimport kotlinx.coroutines.launch\nimport java.util.UUID\n\n@Composable\nfun DynamicItem(\n    modifier: Modifier = Modifier,\n    dynamicItem: DynamicItem,\n    previewerState: ImagePreviewerState = rememberPreviewerState(pageCount = { 0 }),\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit = { _, _ -> },\n    onClick: (DynamicItem) -> Unit = {}\n) {\n    val paddingSize = 12.dp\n\n    Surface(\n        modifier = modifier,\n        onClick = { onClick(dynamicItem) },\n        color = MaterialTheme.colorScheme.surfaceContainerLow,\n    ) {\n        Column(\n            modifier = Modifier.padding(vertical = paddingSize),\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n        ) {\n            DynamicHeader(\n                modifier = Modifier.padding(horizontal = paddingSize),\n                author = dynamicItem.author\n            )\n            DynamicContent(\n                dynamicItem = dynamicItem,\n                horizontalPadding = paddingSize,\n                previewerState = previewerState,\n                onShowPreviewer = onShowPreviewer,\n                onClick = onClick\n            )\n            DynamicFooter(\n                modifier = Modifier.padding(horizontal = paddingSize),\n                footer = dynamicItem.footer!!,\n                isLike = false,\n                onShare = {\n                    //TODO 动态分享按钮\n                    notYetImplemented()\n                },\n                onShowComment = {\n                    //TODO 动态查看评论按钮\n                    notYetImplemented()\n                },\n                onLike = {\n                    //TODO 动态点赞按钮\n                    notYetImplemented()\n                }\n            )\n        }\n    }\n}\n\n@Composable\nfun DynamicContent(\n    modifier: Modifier = Modifier,\n    dynamicItem: DynamicItem,\n    horizontalPadding: Dp = 12.dp,\n    previewerState: ImagePreviewerState = rememberPreviewerState(pageCount = { 0 }),\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit = { _, _ -> },\n    onClick: (DynamicItem) -> Unit\n\n) {\n    val contentModifier = modifier.padding(horizontal = horizontalPadding)\n    when (dynamicItem.type) {\n        DynamicType.Av -> DynamicVideoContent(\n            modifier = contentModifier,\n            video = dynamicItem.video!!\n        )\n\n        DynamicType.Draw -> DynamicDraw(\n            modifier = contentModifier,\n            draw = dynamicItem.draw!!,\n            previewerState = previewerState,\n            onShowPreviewer = onShowPreviewer\n        )\n\n        DynamicType.Forward -> DynamicForward(\n            modifier = modifier,\n            word = dynamicItem.word,\n            dynamicItem = dynamicItem.orig!!,\n            previewerState = previewerState,\n            onShowPreviewer = onShowPreviewer,\n            onClick = { onClick(dynamicItem.orig!!) }\n        )\n\n        DynamicType.LiveRcmd -> DynamicLiveRcmd(\n            modifier = contentModifier,\n            liveRcmd = dynamicItem.liveRcmd!!\n        )\n\n        DynamicType.UgcSeason -> {\n            Text(\"${dynamicItem}\")\n        }\n\n        DynamicType.Word -> DynamicWord(\n            modifier = contentModifier,\n            word = dynamicItem.word!!\n        )\n\n        DynamicType.Pgc -> DynamicPgc(\n            modifier = contentModifier,\n            pgc = dynamicItem.pgc!!\n        )\n\n        DynamicType.Article -> DynamicArticle(\n            modifier = contentModifier,\n            article = dynamicItem.article!!\n        )\n\n        DynamicType.None -> DynamicNone(\n            modifier = contentModifier,\n            none = dynamicItem.none!!\n        )\n    }\n}\n\n@Composable\nfun DynamicVideoContent(\n    modifier: Modifier = Modifier,\n    video: DynamicItem.DynamicVideoModule\n) {\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        if (video.text.isNotBlank()) {\n            Text(text = video.text)\n        }\n        Card(\n            modifier = Modifier\n                .fillMaxWidth()\n                .aspectRatio(1.6f)\n        ) {\n            Box(\n                contentAlignment = Alignment.BottomCenter\n            ) {\n                AsyncImage(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .aspectRatio(1.6f)\n                        .clip(MaterialTheme.shapes.medium),\n                    model = video.cover.resizedImageUrl(ImageSize.SmallVideoCardCover),\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(48.dp)\n                        .background(\n                            Brush.verticalGradient(\n                                colors = listOf(\n                                    Color.Transparent,\n                                    Color.Black.copy(alpha = 0.3f)\n                                )\n                            )\n                        )\n                )\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(12.dp, 8.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        if (video.play.isNotBlank()) {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.spacedBy(2.dp)\n                            ) {\n                                Icon(\n                                    modifier = Modifier,\n                                    painter = painterResource(id = R.drawable.ic_play_count),\n                                    contentDescription = null,\n                                    tint = Color.White\n                                )\n                                Text(\n                                    text = video.play,\n                                    style = MaterialTheme.typography.bodySmall,\n                                    color = Color.White\n                                )\n                            }\n                        }\n                        if (video.danmaku.isNotBlank()) {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.spacedBy(2.dp)\n                            ) {\n                                Icon(\n                                    modifier = Modifier,\n                                    painter = painterResource(id = R.drawable.ic_danmaku_count),\n                                    contentDescription = null,\n                                    tint = Color.White\n                                )\n                                Text(\n                                    text = video.danmaku,\n                                    style = MaterialTheme.typography.bodySmall,\n                                    color = Color.White\n                                )\n                            }\n                        }\n                    }\n                    Text(\n                        text = video.duration,\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White\n                    )\n                }\n            }\n\n        }\n        Text(text = video.title)\n    }\n}\n\n@Composable\nfun DynamicHeader(\n    modifier: Modifier = Modifier,\n    author: DynamicItem.DynamicAuthorModule\n) {\n    Box(\n        modifier = modifier\n            .height(48.dp)\n            .fillMaxWidth()\n    ) {\n        Row(\n            modifier = Modifier\n                .align(Alignment.CenterStart)\n                .fillMaxWidth()\n                .padding(end = 30.dp),\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            UserAvatar(\n                avatar = author.avatar,\n                size = 48.dp\n            )\n            Column(\n                modifier = Modifier.fillMaxHeight(),\n                verticalArrangement = Arrangement.SpaceAround\n            ) {\n                Text(\n                    text = author.author,\n                    maxLines = 1\n                )\n                Text(\n                    text = author.pubTime + \" ${author.pubAction}\",\n                    maxLines = 1,\n                    color = MaterialTheme.colorScheme.onSurface.copy(0.8f),\n                    fontSize = 14.sp,\n                    lineHeight = 14.sp\n                )\n            }\n        }\n\n        IconButton(\n            modifier = Modifier\n                .align(Alignment.CenterEnd)\n                .size(30.dp),\n            onClick = {\n                //TODO 动态右上角按钮\n                notYetImplemented()\n            }\n        ) {\n            Icon(imageVector = Icons.Default.MoreVert, contentDescription = \"Menu\")\n        }\n    }\n}\n\n@Composable\nfun DynamicForwardHeader(\n    modifier: Modifier = Modifier,\n    author: DynamicItem.DynamicAuthorModule\n) {\n    Box(\n        modifier = modifier\n            .height(24.dp)\n            .fillMaxWidth()\n    ) {\n        Row(\n            modifier = Modifier\n                .align(Alignment.CenterStart)\n                .fillMaxWidth(),\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            UserAvatar(\n                avatar = author.avatar,\n                size = 20.dp\n            )\n\n            Text(\n                text = author.author,\n                maxLines = 1,\n                fontSize = 14.sp,\n                lineHeight = 14.sp\n            )\n            Text(\n                text = author.pubTime + \" ${author.pubAction}\",\n                maxLines = 1,\n                color = MaterialTheme.colorScheme.onSurface.copy(0.8f),\n                fontSize = 14.sp,\n                lineHeight = 14.sp\n            )\n        }\n    }\n}\n\n@Composable\nfun DynamicFooter(\n    modifier: Modifier = Modifier,\n    footer: DynamicItem.DynamicFooterModule,\n    isLike: Boolean = false,\n    onShare: (() -> Unit)? = null,\n    onShowComment: (() -> Unit)? = null,\n    onLike: (() -> Unit)? = null\n) {\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .height(40.dp),\n        horizontalArrangement = Arrangement.SpaceAround,\n    ) {\n        DynamicFooterButton(\n            icon = Icons.Default.Share,\n            number = footer.share\n        ) { onShare?.invoke() }\n        DynamicFooterButton(\n            icon = Icons.AutoMirrored.Filled.Comment,\n            number = footer.comment\n        ) { onShowComment?.invoke() }\n        DynamicFooterButton(\n            icon = if (isLike) Icons.Default.Favorite else Icons.Default.FavoriteBorder,\n            number = footer.like\n        ) { onLike?.invoke() }\n    }\n}\n\n@Composable\nfun DynamicFooterButton(\n    modifier: Modifier = Modifier,\n    icon: ImageVector,\n    number: Int,\n    onClick: () -> Unit\n) {\n    TextButton(\n        modifier = modifier,\n        onClick = onClick\n    ) {\n        Icon(\n            modifier = Modifier.size(16.dp),\n            imageVector = icon,\n            contentDescription = null\n        )\n        Text(\n            text = number.toString(),\n            modifier = Modifier.padding(start = 4.dp)\n        )\n    }\n}\n\n//TODO 富文本\n@Composable\nfun DynamicDraw(\n    modifier: Modifier = Modifier,\n    draw: DynamicItem.DynamicDrawModule,\n    previewerState: ImagePreviewerState,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit\n) {\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        if (draw.title != null) {\n            Text(\n                text = draw.title!!,\n                fontWeight = FontWeight.Bold\n            )\n        }\n        Text(text = draw.text)\n        DynamicPictures(\n            pictures = draw.images,\n            previewerState = previewerState,\n            onShowPreviewer = onShowPreviewer\n        )\n    }\n}\n\n\n@Composable\nfun DynamicPictures(\n    modifier: Modifier = Modifier,\n    pictures: List<Picture>,\n    previewerState: ImagePreviewerState,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit,\n) {\n    val scope = rememberCoroutineScope()\n    val imageBaseShape = MaterialTheme.shapes.medium\n\n    val onClickPicture: (index: Int, itemState: TransformItemState) -> Unit = { index, itemState ->\n        onShowPreviewer(pictures) {\n            scope.launch {\n                previewerState.openTransform(\n                    index = index,\n                    itemState = itemState,\n                )\n            }\n        }\n    }\n\n    Box(\n        modifier = modifier\n    ) {\n        when {\n            pictures.size == 1 -> {\n                Row {\n                    val itemState = rememberTransformItemState()\n                    Card(\n                        modifier = Modifier\n                            .weight(1f)\n                            .aspectRatio(2f),\n                        shape = imageBaseShape,\n                        onClick = {\n                            onClickPicture(0, itemState)\n                        }\n                    ) {\n                        TransformImageView(\n                            painter = rememberAsyncImagePainter(pictures.first().url),\n                            key = pictures.first().key,\n                            itemState = itemState,\n                            previewerState = previewerState,\n                        )\n                    }\n                }\n            }\n\n            pictures.size == 2 -> {\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    pictures.forEachIndexed { index, picture ->\n                        val itemState = rememberTransformItemState()\n                        Card(\n                            modifier = Modifier\n                                .weight(1f)\n                                .aspectRatio(1f),\n                            shape = when (index) {\n                                0 -> imageBaseShape.copy(\n                                    topEnd = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)\n                                )\n\n                                1 -> imageBaseShape.copy(\n                                    topStart = CornerSize(0.dp), bottomStart = CornerSize(0.dp)\n                                )\n\n                                else -> RoundedCornerShape(0.dp)\n                            },\n                            onClick = {\n                                onClickPicture(index, itemState)\n                            }\n                        ) {\n                            TransformImageView(\n                                painter = rememberAsyncImagePainter(picture.url),\n                                key = picture.key,\n                                itemState = itemState,\n                                previewerState = previewerState,\n                            )\n                        }\n                    }\n                }\n            }\n\n            pictures.size >= 3 -> {\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    pictures.take(3).forEachIndexed { index, picture ->\n                        val itemState = rememberTransformItemState()\n                        Card(\n                            modifier = Modifier\n                                .weight(1f)\n                                .aspectRatio(1f),\n                            shape = when (index) {\n                                0 -> imageBaseShape.copy(\n                                    topEnd = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)\n                                )\n\n                                2 -> imageBaseShape.copy(\n                                    topStart = CornerSize(0.dp), bottomStart = CornerSize(0.dp)\n                                )\n\n                                else -> RoundedCornerShape(0.dp)\n                            },\n                            onClick = {\n                                onClickPicture(index, itemState)\n                            }\n                        ) {\n                            TransformImageView(\n                                painter = rememberAsyncImagePainter(picture.url),\n                                key = picture.key,\n                                itemState = itemState,\n                                previewerState = previewerState,\n                            )\n                        }\n                    }\n                }\n\n                if (pictures.size > 3) {\n                    Text(\n                        modifier = Modifier\n                            .align(Alignment.BottomEnd)\n                            .clip(\n                                MaterialTheme.shapes.medium.copy(\n                                    topEnd = CornerSize(0.dp),\n                                    bottomStart = CornerSize(0.dp)\n                                )\n                            )\n                            .background(Color.Black.copy(alpha = 0.2f))\n                            .padding(horizontal = 8.dp),\n                        text = \"+${pictures.size - 3}\",\n                        color = Color.White\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun DynamicWord(\n    modifier: Modifier = Modifier,\n    word: DynamicItem.DynamicWordModule\n) {\n    Text(\n        modifier = modifier,\n        text = word.text\n    )\n}\n\n@Composable\nfun DynamicForward(\n    modifier: Modifier = Modifier,\n    word: DynamicItem.DynamicWordModule?,\n    dynamicItem: DynamicItem,\n    previewerState: ImagePreviewerState,\n    horizontalPadding: Dp = 12.dp,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit,\n    onClick: () -> Unit\n) {\n    Column(\n        modifier = modifier,\n    ) {\n        if (word != null) {\n            Text(\n                modifier = Modifier.padding(horizontal = horizontalPadding),\n                text = word.text\n            )\n        }\n        Surface(\n            color = MaterialTheme.colorScheme.surfaceContainer,\n            onClick = onClick\n        ) {\n            Box(\n                modifier = Modifier.padding(horizontal = horizontalPadding, vertical = 6.dp),\n            ) {\n                Column(\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    if (dynamicItem.author.mid != -1L) {\n                        DynamicForwardHeader(\n                            author = dynamicItem.author\n                        )\n                    }\n                    DynamicContent(\n                        modifier = Modifier.fillMaxWidth(),\n                        dynamicItem = dynamicItem,\n                        horizontalPadding = 0.dp,\n                        previewerState = previewerState,\n                        onShowPreviewer = onShowPreviewer,\n                        onClick = {}\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun DynamicLiveRcmd(\n    modifier: Modifier = Modifier,\n    liveRcmd: DynamicItem.DynamicLiveRcmdModule\n) {\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        Card(\n            modifier = Modifier\n                .fillMaxWidth()\n                .aspectRatio(1.6f)\n        ) {\n            Box(\n                contentAlignment = Alignment.BottomCenter\n            ) {\n                AsyncImage(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .aspectRatio(1.6f)\n                        .clip(MaterialTheme.shapes.medium),\n                    model = liveRcmd.cover.resizedImageUrl(ImageSize.SmallVideoCardCover),\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(48.dp)\n                        .background(\n                            Brush.verticalGradient(\n                                colors = listOf(\n                                    Color.Transparent,\n                                    Color.Black.copy(alpha = 0.3f)\n                                )\n                            )\n                        )\n                )\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(12.dp, 8.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n\n                    Text(\n                        text = \"${liveRcmd.roomId}\",\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White\n                    )\n                }\n            }\n        }\n        Text(text = liveRcmd.title)\n    }\n}\n\n@Composable\nfun DynamicPgc(\n    modifier: Modifier = Modifier,\n    pgc: DynamicItem.DynamicPgcModule\n) {\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        Card(\n            modifier = Modifier\n                .fillMaxWidth()\n                .aspectRatio(1.6f)\n        ) {\n            AsyncImage(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .aspectRatio(1.6f)\n                    .clip(MaterialTheme.shapes.medium),\n                model = pgc.cover.resizedImageUrl(ImageSize.SmallVideoCardCover),\n                contentDescription = null,\n                contentScale = ContentScale.FillBounds\n            )\n        }\n        Text(text = pgc.title)\n    }\n}\n\n@Composable\nfun DynamicArticle(\n    modifier: Modifier = Modifier,\n    article: DynamicItem.DynamicArticleModule\n) {\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        Text(\n            text = article.title,\n            fontWeight = FontWeight.Bold\n        )\n        Text(text = article.text)\n        if (article.covers.isNotEmpty()) {\n            AsyncImage(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .clip(MaterialTheme.shapes.medium),\n                model = article.covers.first().resizedImageUrl(ImageSize.SmallVideoCardCover),\n                contentDescription = null,\n                contentScale = ContentScale.FillBounds\n            )\n        }\n    }\n}\n\n@Composable\nfun DynamicNone(\n    modifier: Modifier = Modifier,\n    none: DynamicItem.DynamicNoneModule\n) {\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        Text(text = none.text)\n    }\n}\n\n@Preview\n@Composable\nprivate fun DynamicHeaderPreview() {\n    BVMobileTheme {\n        Surface {\n            DynamicHeader(\n                author = emptyDynamicVideoData.author\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun DynamicForwardHeaderPreview() {\n    BVMobileTheme {\n        Surface {\n            DynamicForwardHeader(\n                author = emptyDynamicVideoData.author\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun DynamicFooterPreview() {\n    BVMobileTheme {\n        Surface {\n            DynamicFooter(\n                footer = exampleFooterData\n            )\n        }\n    }\n}\n\nprivate val exampleAuthorData = DynamicItem.DynamicAuthorModule(\n    author = \"author\",\n    avatar = \"\",\n    mid = 0,\n    pubTime = \"54 分钟前 投稿了视频\",\n    pubAction = \"\"\n)\n\nprivate val exampleFooterData = DynamicItem.DynamicFooterModule(\n    like = 2,\n    comment = 61,\n    share = 8,\n)\n\nprivate val exampleVideoData = DynamicItem.DynamicVideoModule(\n    aid = 0,\n    title = \"title\",\n    cover = \"\",\n    duration = \"23:45\",\n    play = \"xx play\",\n    danmaku = \"xx dm\",\n    seasonId = 0,\n    cid = 0,\n    text = \"desc\"\n)\n\nprivate val emptyDynamicData = DynamicItem(\n    type = DynamicType.Av,\n    author = exampleAuthorData,\n    footer = exampleFooterData\n)\n\nprivate val emptyDynamicVideoData = DynamicItem(\n    type = DynamicType.Av,\n    author = exampleAuthorData,\n    video = exampleVideoData,\n    footer = exampleFooterData\n)\n\nprivate val emptyDynamicDrawData = DynamicItem(\n    type = DynamicType.Draw,\n    author = exampleAuthorData,\n    draw = DynamicItem.DynamicDrawModule(\n        title = \"title\",\n        text = \"draw\",\n        images = emptyList()\n    ),\n    footer = exampleFooterData\n)\n\nprivate val emptyDynamicWordData = DynamicItem.DynamicWordModule(\n    text = \"this is word module\"\n)\n\nprivate val exampleDynamicForwardData = DynamicItem(\n    type = DynamicType.Forward,\n    author = exampleAuthorData,\n    orig = emptyDynamicVideoData,\n    word = emptyDynamicWordData,\n    footer = exampleFooterData\n)\n\nprivate val exampleDynamicForwardNoneData = DynamicItem(\n    type = DynamicType.Forward,\n    author = exampleAuthorData,\n    orig = DynamicItem(\n        type = DynamicType.None,\n        author = DynamicItem.DynamicAuthorModule(\"\", \"\", -1, \"\", \"\"),\n        none = DynamicItem.DynamicNoneModule(\"unknown dynamic\")\n    ),\n    word = emptyDynamicWordData,\n    footer = exampleFooterData\n)\n\nprivate val exampleDynamicLiveRcmdData = DynamicItem(\n    type = DynamicType.LiveRcmd,\n    author = exampleAuthorData,\n    liveRcmd = DynamicItem.DynamicLiveRcmdModule(\n        cover = \"\",\n        title = \"title\",\n        roomId = 3\n    ),\n    footer = exampleFooterData\n)\n\nprivate val exampleDynamicPgcData = DynamicItem(\n    type = DynamicType.Pgc,\n    author = exampleAuthorData,\n    pgc = DynamicItem.DynamicPgcModule(\n        cover = \"\",\n        title = \"title\",\n        seasonId = 3,\n        epid = 3,\n        aid = 0,\n        cid = 0\n    ),\n    footer = exampleFooterData\n)\n\nprivate val exampleDynamicArticleData = DynamicItem(\n    type = DynamicType.Article,\n    author = exampleAuthorData,\n    article = DynamicItem.DynamicArticleModule(\n        title = \"title\",\n        text = \"article content\",\n        covers = listOf(\"\"),\n        id = 0,\n        url = \"\",\n        label = \"\"\n    ),\n    footer = exampleFooterData\n)\n\n@Preview\n@Composable\nprivate fun DynamicVideoItemPreview() {\n    BVMobileTheme {\n        Surface {\n            DynamicItem(\n                modifier = Modifier.padding(vertical = 8.dp),\n                dynamicItem = emptyDynamicVideoData\n            )\n        }\n    }\n}\n\nprivate class DynamicDrawItemProvider : PreviewParameterProvider<DynamicItem> {\n    override val values = List(5) { index ->\n        emptyDynamicData.copy(\n            type = DynamicType.Draw,\n            draw = DynamicItem.DynamicDrawModule(\n                title = \"title\",\n                text = \"this is $index picture draw\",\n                images = Array(index) { Picture(\"\", 0, 0, \"${UUID.randomUUID()}\") }.toList()\n            )\n        )\n    }.asSequence()\n}\n\n@Preview\n@Composable\nprivate fun DynamicDrawItemPreview(@PreviewParameter(DynamicDrawItemProvider::class) dynamicItem: DynamicItem) {\n    BVMobileTheme {\n        Surface {\n            DynamicItem(\n                modifier = Modifier.padding(vertical = 8.dp),\n                dynamicItem = dynamicItem\n            )\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun DynamicForwardItemPreview() {\n    BVMobileTheme {\n        Surface {\n            DynamicItem(\n                modifier = Modifier.padding(vertical = 8.dp),\n                dynamicItem = exampleDynamicForwardData\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun DynamicForwardItemNonePreview() {\n    BVMobileTheme {\n        Surface {\n            DynamicItem(\n                modifier = Modifier.padding(vertical = 8.dp),\n                dynamicItem = exampleDynamicForwardNoneData\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun DynamicLiveRcmdItemPreview() {\n    BVMobileTheme {\n        Surface {\n            DynamicItem(\n                modifier = Modifier.padding(vertical = 8.dp),\n                dynamicItem = exampleDynamicLiveRcmdData\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun DynamicItemListPreview() {\n    BVMobileTheme {\n        Surface {\n            LazyColumn(\n                modifier = Modifier.background(MaterialTheme.colorScheme.surfaceVariant)\n            ) {\n                items(3) {\n                    DynamicItem(\n                        modifier = Modifier.padding(bottom = 8.dp),\n                        dynamicItem = emptyDynamicVideoData\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun DynamicPgcItemPreview() {\n    BVMobileTheme {\n        Surface {\n            DynamicItem(\n                modifier = Modifier.padding(vertical = 8.dp),\n                dynamicItem = exampleDynamicPgcData\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun DynamicArticleItemPreview() {\n    BVMobileTheme {\n        Surface {\n            DynamicItem(\n                modifier = Modifier.padding(vertical = 8.dp),\n                dynamicItem = exampleDynamicArticleData\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/home/dynamic/DynamicUserItem.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.home.dynamic\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.mobile.component.user.UserAvatar\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@Composable\nfun DynamicUserItem(\n    modifier: Modifier = Modifier,\n    avatar: String,\n    username: String\n) {\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        UserAvatar(avatar = avatar)\n        Text(\n            modifier = Modifier.width(80.dp),\n            text = username,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis,\n            textAlign = TextAlign.Center,\n            color = MaterialTheme.colorScheme.onSurface\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun DynamicUserItemPreview() {\n    BVMobileTheme {\n        Surface {\n            DynamicUserItem(\n                avatar = \"https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg\",\n                username = \"bishi\"\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/player/VideoPlayerPages.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.player\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.foundation.text.appendInlineContent\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ChevronRight\nimport androidx.compose.material.icons.filled.Done\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FilterChipDefaults\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.SecondaryScrollableTabRow\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Tab\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.Placeholder\nimport androidx.compose.ui.text.PlaceholderVerticalAlign\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.core.graphics.BlendModeColorFilterCompat\nimport androidx.core.graphics.BlendModeCompat\nimport com.airbnb.lottie.LottieProperty\nimport com.airbnb.lottie.compose.LottieAnimation\nimport com.airbnb.lottie.compose.LottieCompositionSpec\nimport com.airbnb.lottie.compose.LottieConstants\nimport com.airbnb.lottie.compose.animateLottieCompositionAsState\nimport com.airbnb.lottie.compose.rememberLottieComposition\nimport com.airbnb.lottie.compose.rememberLottieDynamicProperties\nimport com.airbnb.lottie.compose.rememberLottieDynamicProperty\nimport dev.aaa1115910.biliapi.entity.video.Dimension\nimport dev.aaa1115910.biliapi.entity.video.VideoPage\nimport dev.aaa1115910.biliapi.entity.video.season.Episode\nimport dev.aaa1115910.biliapi.entity.video.season.Section\nimport dev.aaa1115910.biliapi.entity.video.season.UgcSeason\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.formatHourMinSec\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun VideoPlayerPages(\n    modifier: Modifier = Modifier,\n    currentCid: Long,\n    pages: List<VideoPage>,\n    ugcSeason: UgcSeason?,\n    pgcSections: List<Section>,\n    onClickPage: (VideoPage) -> Unit,\n    onClickEpisode: (sectionIndex: Int, episode: Episode) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val sheetState = rememberModalBottomSheetState(\n        skipPartiallyExpanded = true,\n        confirmValueChange = { sheetValue ->\n            println(\"confirmValueChange: $sheetValue\")\n            true\n        }\n    )\n    var openBottomSheet by rememberSaveable { mutableStateOf(false) }\n\n    var currentSection by remember { mutableStateOf<Section?>(null) }\n\n    LaunchedEffect(currentCid) {\n        if (pgcSections.isNotEmpty()) {\n            currentSection =\n                pgcSections.find { it.episodes.any { episode -> episode.cid == currentCid } }\n        } else if (ugcSeason != null) {\n            currentSection = ugcSeason.sections.find {\n                it.episodes.any { episode ->\n                    episode.cid == currentCid || episode.pages.any { page -> page.cid == currentCid }\n                }\n            }\n        }\n    }\n\n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .background(MaterialTheme.colorScheme.surfaceContainer)\n    ) {\n        if (pgcSections.isNotEmpty()) {\n            // TODO pgc\n        } else if (ugcSeason != null) {\n            // TODO ugc\n            if (currentSection != null) {\n                //VideoPlayerUgcSectionsFilter(\n                //    sections = ugcSeason.sections,\n                //    currentSection = currentSection!!,\n                //    onSectionChange = { currentSection = it }\n                //)\n                VideoPlayerEpisodesRow(\n                    //title = currentSection!!.title,\n                    episodes = currentSection!!.episodes,\n                    onClickMore = { openBottomSheet = !openBottomSheet },\n                    onClickEpisode = { episode ->\n                        onClickEpisode(\n                            ugcSeason.sections.indexOf(currentSection), episode\n                        )\n                    },\n                    currentCid = currentCid\n                )\n            }\n        } else if (pages.size > 1) {\n            VideoPlayerPagesRow(\n                //title = \"视频分 P\",\n                pages = pages,\n                onClickMore = { openBottomSheet = !openBottomSheet },\n                onClickPage = onClickPage,\n                currentCid = currentCid\n            )\n        }\n    }\n\n\n    if (openBottomSheet) {\n        ModalBottomSheet(\n            sheetState = sheetState,\n            onDismissRequest = { openBottomSheet = false },\n            contentWindowInsets = { WindowInsets(0, 0, 0, 0) }\n        ) {\n            VideoPlayerPartSheetContent(\n                currentCid = currentCid,\n                pages = pages,\n                ugcSeason = ugcSeason,\n                pgcSections = pgcSections,\n                onClickPage = onClickPage,\n                onClickEpisode = { episode ->\n                    onClickEpisode(\n                        ugcSeason!!.sections.indexOf(currentSection), episode\n                    )\n                }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun VideoPlayerUgcSectionsFilter(\n    modifier: Modifier = Modifier,\n    sections: List<Section>,\n    currentSection: Section,\n    onSectionChange: (Section) -> Unit = {}\n) {\n    LazyRow {\n        items(sections) { section ->\n            VideoPlayerUgcSectionsFilterChip(\n                modifier = modifier,\n                section = section,\n                selected = section == currentSection,\n                onClick = { onSectionChange(section) }\n            )\n        }\n    }\n}\n\n@Composable\nfun VideoPlayerUgcSectionsFilterChip(\n    modifier: Modifier = Modifier,\n    section: Section,\n    selected: Boolean,\n    onClick: () -> Unit\n) {\n    FilterChip(\n        modifier = modifier,\n        onClick = onClick,\n        label = {\n            Text(\n                text = section.title,\n                style = MaterialTheme.typography.titleSmall\n            )\n        },\n        selected = selected,\n        leadingIcon = (@Composable {\n            Icon(\n                imageVector = Icons.Filled.Done,\n                contentDescription = null,\n                modifier = Modifier.size(FilterChipDefaults.IconSize)\n            )\n        }).takeIf { selected }\n    )\n}\n\n@Composable\nfun VideoPlayerEpisodesRow(\n    modifier: Modifier = Modifier,\n    title: String? = null,\n    episodes: List<Episode>,\n    currentCid: Long,\n    onClickMore: () -> Unit = {},\n    onClickEpisode: (Episode) -> Unit = {}\n) {\n    Column(\n        modifier = modifier\n            .background(MaterialTheme.colorScheme.surface)\n    ) {\n        if (title != null) {\n            Text(\n                modifier = Modifier.padding(horizontal = 8.dp),\n                text = title,\n                style = MaterialTheme.typography.titleSmall\n            )\n        }\n        Box {\n            LazyRow(\n                modifier = Modifier.fillMaxWidth(),\n                contentPadding = PaddingValues(\n                    start = 8.dp,\n                    end = 68.dp,\n                    top = 8.dp,\n                    bottom = 8.dp\n                ),\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                itemsIndexed(episodes) { index, episode ->\n                    VideoPlayerPageItem(\n                        modifier = modifier,\n                        text = \"EP${index + 1} ${episode.title}\",\n                        onClick = { onClickEpisode(episode) },\n                        isPlaying = episode.cid == currentCid\n                    )\n                }\n            }\n            MoreButton(\n                modifier = Modifier\n                    .align(Alignment.CenterEnd),\n                onClick = onClickMore\n            )\n        }\n    }\n}\n\n@Composable\nfun VideoPlayerPagesRow(\n    modifier: Modifier = Modifier,\n    title: String? = null,\n    pages: List<VideoPage>,\n    currentCid: Long,\n    onClickMore: () -> Unit = {},\n    onClickPage: (VideoPage) -> Unit = {}\n) {\n    Column(\n        modifier = modifier\n            .background(MaterialTheme.colorScheme.surface)\n    ) {\n        if (title != null) {\n            Text(\n                modifier = Modifier.padding(horizontal = 8.dp),\n                text = title,\n                style = MaterialTheme.typography.titleSmall\n            )\n        }\n        Box(\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            LazyRow(\n                contentPadding = PaddingValues(\n                    start = 8.dp,\n                    end = 68.dp,\n                    top = 8.dp,\n                    bottom = 8.dp\n                ),\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                itemsIndexed(pages) { index, page ->\n                    VideoPlayerPageItem(\n                        modifier = modifier,\n                        text = \"P${index + 1} ${page.title}\",\n                        onClick = { onClickPage(page) },\n                        isPlaying = page.cid == currentCid\n                    )\n                }\n            }\n            MoreButton(\n                modifier = Modifier\n                    .align(Alignment.CenterEnd),\n                onClick = onClickMore\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun MoreButton(\n    modifier: Modifier = Modifier,\n    onClick: () -> Unit = {}\n) {\n    var color by remember { mutableStateOf(Color.Red) }\n    color = MaterialTheme.colorScheme.surface\n    val colorStops = arrayOf(\n        0.0f to Color.Transparent,\n        0.4f to MaterialTheme.colorScheme.surface,\n        1f to MaterialTheme.colorScheme.surface\n    )\n    Box(\n        modifier = modifier\n            .width(60.dp)\n            .height(80.dp)\n            .background(Brush.horizontalGradient(colorStops = colorStops)),\n        contentAlignment = Alignment.Center\n    ) {\n        IconButton(\n            modifier = Modifier\n                .align(Alignment.Center)\n                .offset(x = 8.dp),\n            onClick = onClick\n        ) {\n            Icon(imageVector = Icons.Default.ChevronRight, contentDescription = null)\n        }\n    }\n}\n\n@Composable\nprivate fun VideoPlayerPageItem(\n    modifier: Modifier = Modifier,\n    text: String,\n    isPlaying: Boolean,\n    onClick: () -> Unit\n) {\n    val density = LocalDensity.current\n    val inlineContentMap = mapOf(\n        \"playingIcon\" to InlineTextContent(\n            Placeholder(\n                width = with(density) { 20.dp.toSp() },\n                height = with(density) { 20.dp.toSp() },\n                placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter\n            )\n        ) {\n            PlayingIcon()\n        }\n    )\n    val annotatedString = buildAnnotatedString {\n        if (isPlaying) appendInlineContent(\"playingIcon\")\n        append(text)\n    }\n    Box(\n        modifier = modifier\n            .width(160.dp)\n            .clip(MaterialTheme.shapes.medium)\n            .background(MaterialTheme.colorScheme.surfaceContainer)\n            .clickable { onClick() }\n    ) {\n        Text(\n            modifier = Modifier.padding(8.dp),\n            text = annotatedString,\n            maxLines = 2,\n            minLines = 2,\n            overflow = TextOverflow.Ellipsis,\n            inlineContent = inlineContentMap,\n            color = if (isPlaying) MaterialTheme.colorScheme.primary else Color.Unspecified\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun VideoPlayerPartSheetContent(\n    modifier: Modifier = Modifier,\n    currentCid: Long,\n    pages: List<VideoPage>,\n    ugcSeason: UgcSeason?,\n    pgcSections: List<Section>,\n    onClickPage: (VideoPage) -> Unit,\n    onClickEpisode: (Episode) -> Unit\n) {\n    var currentSection by remember { mutableStateOf(ugcSeason?.sections?.first()) }\n\n    val onClickSectionTab: (Section) -> Unit = { section ->\n        currentSection = section\n    }\n\n    LaunchedEffect(currentCid) {\n        if (pgcSections.isNotEmpty()) {\n            currentSection =\n                pgcSections.find { it.episodes.any { episode -> episode.cid == currentCid } }\n        } else if (ugcSeason != null) {\n            currentSection = ugcSeason.sections.find {\n                it.episodes.any { episode ->\n                    episode.cid == currentCid || episode.pages.any { page -> page.cid == currentCid }\n                }\n            }\n        }\n    }\n\n    Column(\n        modifier = modifier\n            .fillMaxSize()\n            .background(MaterialTheme.colorScheme.surface)\n    ) {\n        Row {\n            TopAppBar(\n                title = {\n                    Text(\n                        text = if (pgcSections.isNotEmpty()) {\n                            \"视频选集\"\n                        } else if (ugcSeason != null) {\n                            \"视频选集\"\n                        } else {\n                            \"视频分 P\"\n                        },\n                    )\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                ),\n                windowInsets = WindowInsets(0, 0, 0, 0)\n            )\n        }\n        //Text(\"ugcSeason: $ugcSeason\")\n        if (pgcSections.isNotEmpty()) {\n            // TODO pgc\n            Text(\"pgc\")\n        } else if (ugcSeason != null) {\n            // TODO ugc\n            if (currentSection != null) {\n                if (ugcSeason.sections.size > 1) {\n                    SecondaryScrollableTabRow(\n                        selectedTabIndex = ugcSeason.sections.indexOf(currentSection!!),\n                        containerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                        divider = {}\n                    ) {\n                        ugcSeason.sections.forEach { section ->\n                            Tab(\n                                selected = currentSection == section,\n                                onClick = { onClickSectionTab(section) }\n                            ) {\n                                Box(\n                                    modifier = Modifier.height(48.dp),\n                                    contentAlignment = Alignment.Center\n                                ) {\n                                    Text(\n                                        modifier = Modifier.padding(horizontal = 16.dp),\n                                        text = section.title,\n                                        style = MaterialTheme.typography.bodyLarge,\n                                        textAlign = TextAlign.Center\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n                HorizontalDivider()\n                LazyColumn(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .background(MaterialTheme.colorScheme.surface),\n                    contentPadding = PaddingValues(vertical = 8.dp)\n                ) {\n                    itemsIndexed(currentSection!!.episodes) { epIndex, episode ->\n                        if (episode.pages.size <= 1) {\n                            PageListItem(\n                                modifier = modifier,\n                                text = \"EP${epIndex + 1} ${episode.title}\",\n                                duration = episode.duration,\n                                isPlaying = episode.cid == currentCid,\n                                onClick = { onClickEpisode(episode) }\n                            )\n                        } else {\n                            Column {\n                                var expand by remember { mutableStateOf(true) }\n                                LaunchedEffect(currentSection) { expand = true }\n                                PageListItem(\n                                    modifier = modifier,\n                                    text = \"EP${epIndex + 1} ${episode.title}\",\n                                    duration = null,\n                                    isPlaying = episode.pages.any { it.cid == currentCid },\n                                    onClick = { expand = !expand }\n                                )\n                                AnimatedVisibility(\n                                    visible = expand\n                                ) {\n                                    Column(\n                                        modifier = Modifier\n                                            .padding(start = 16.dp)\n                                    ) {\n                                        episode.pages.forEachIndexed { pageIndex, page ->\n                                            PageListItem(\n                                                modifier = modifier,\n                                                text = \"P${pageIndex + 1} ${page.title}\",\n                                                duration = page.duration,\n                                                isPlaying = page.cid == currentCid,\n                                                onClick = { onClickPage(page) }\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    item { Spacer(modifier = Modifier.navigationBarsPadding()) }\n                }\n            }\n        } else if (pages.size > 1) {\n            HorizontalDivider()\n            LazyColumn {\n                itemsIndexed(pages) { index, page ->\n                    PageListItem(\n                        modifier = modifier,\n                        text = \"P${index + 1} ${page.title}\",\n                        duration = page.duration,\n                        isPlaying = page.cid == currentCid,\n                        onClick = { onClickPage(page) }\n                    )\n                }\n                item { Spacer(modifier = Modifier.navigationBarsPadding()) }\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun PageListItem(\n    modifier: Modifier = Modifier,\n    text: String,\n    duration: Int?,\n    isPlaying: Boolean,\n    onClick: () -> Unit = {}\n) {\n    val density = LocalDensity.current\n    val inlineContentMap = mapOf(\n        \"playingIcon\" to InlineTextContent(\n            Placeholder(\n                width = with(density) { 20.dp.toSp() },\n                height = with(density) { 20.dp.toSp() },\n                placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter\n            )\n        ) {\n            PlayingIcon()\n        }\n    )\n    val annotatedString = buildAnnotatedString {\n        if (isPlaying) appendInlineContent(\"playingIcon\")\n        append(text)\n    }\n    ListItem(\n        modifier = modifier\n            .height(40.dp)\n            .clip(MaterialTheme.shapes.medium)\n            .clickable { onClick() },\n        headlineContent = {\n            Text(\n                text = annotatedString,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                inlineContent = inlineContentMap,\n            )\n        },\n        trailingContent = (@Composable {\n            Text(\n                text = (1000 * (duration?.toLong() ?: 0)).formatHourMinSec(),\n                style = MaterialTheme.typography.bodySmall.copy(\n                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                )\n            )\n        }).takeIf { duration != null },\n        colors = ListItemDefaults.colors(\n            headlineColor = if (isPlaying) MaterialTheme.colorScheme.primary else Color.Unspecified,\n            containerColor = Color.Transparent\n        ),\n    )\n}\n\n@Composable\nprivate fun PlayingIcon(modifier: Modifier = Modifier) {\n    val dynamicProperties = rememberLottieDynamicProperties(\n        rememberLottieDynamicProperty(\n            property = LottieProperty.COLOR_FILTER,\n            value = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(\n                MaterialTheme.colorScheme.primary.hashCode(),\n                BlendModeCompat.SRC_ATOP\n            ),\n            keyPath = arrayOf(\n                \"**\"\n            )\n        )\n    )\n\n    val composition by rememberLottieComposition(\n        LottieCompositionSpec.RawRes(R.raw.ic_playing)\n    )\n    val progress by animateLottieCompositionAsState(\n        composition,\n        iterations = LottieConstants.IterateForever\n    )\n\n    LottieAnimation(\n        modifier = Modifier\n            .size(20.dp)\n            .scale(2f),\n        composition = composition,\n        progress = { progress },\n        dynamicProperties = dynamicProperties,\n        clipTextToBoundingBox = true\n    )\n}\n\n@Preview\n@Composable\nprivate fun VideoPlayerPageWithoutTitlePreview() {\n    BVMobileTheme {\n        VideoPlayerPagesRow(\n            pages = List(10) {\n                VideoPage(\n                    cid = it.toLong(),\n                    index = it,\n                    title = \"Page title $it\",\n                    duration = 1,\n                    dimension = Dimension(0, 0)\n                )\n            },\n            currentCid = 0\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun VideoPlayerPageWithTitlePreview() {\n    BVMobileTheme {\n        VideoPlayerPagesRow(\n            title = \"Title\",\n            pages = List(10) { VideoPage(it.toLong(), it, \"Title\", 1, Dimension(0, 0)) },\n            currentCid = 0\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun PlayingIconPreview() {\n    PlayingIcon()\n}\n\n@Preview\n@Composable\nprivate fun PageListPreview() {\n    BVMobileTheme {\n        Surface {\n            Column {\n                repeat(10) {\n                    PageListItem(\n                        isPlaying = it == 0,\n                        duration = 233,\n                        text = \"This is  a page list item title\"\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun VideoPlayerPartSheetContentPagesPreview() {\n    val pages = List(10) {\n        VideoPage(\n            cid = it.toLong(),\n            index = it,\n            title = \"Page title $it\",\n            duration = 1,\n            dimension = Dimension(0, 0)\n        )\n    }\n    BVMobileTheme {\n        VideoPlayerPartSheetContent(\n            currentCid = 1,\n            pages = pages,\n            ugcSeason = null,\n            pgcSections = emptyList(),\n            onClickPage = {},\n            onClickEpisode = {}\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun VideoPlayerPartSheetContentUgcSeasonPreview() {\n    val ugcSeason by remember {\n        mutableStateOf(UgcSeason(\n            id = 0,\n            title = \"Ugc Season Title\",\n            cover = \"\",\n            sections = List(3) { sectionIndex ->\n                Section(\n                    id = sectionIndex.toLong(),\n                    title = \"Section $sectionIndex\",\n                    episodes = List(10) { episodeIndex ->\n                        Episode(\n                            id = episodeIndex,\n                            cid = episodeIndex.toLong(),\n                            title = \"Section $sectionIndex Episode $episodeIndex\",\n                            aid = episodeIndex.toLong(),\n                            bvid = \"\",\n                            longTitle = \"Episode long title $episodeIndex\",\n                            cover = \"\",\n                            duration = 111,\n                            dimension = Dimension(0, 0),\n                            pages = if (episodeIndex == 3) {\n                                List(10) { pageIndex ->\n                                    VideoPage(\n                                        cid = 100 + pageIndex.toLong(),\n                                        index = pageIndex,\n                                        title = \"Pages in sections $pageIndex\",\n                                        duration = 100,\n                                        dimension = Dimension(0, 0)\n                                    )\n                                }\n                            } else {\n                                emptyList()\n                            }\n                        )\n                    }\n                )\n            }\n        ))\n    }\n    BVMobileTheme {\n        VideoPlayerPartSheetContent(\n            currentCid = 102,\n            pages = emptyList(),\n            ugcSeason = ugcSeason,\n            pgcSections = emptyList(),\n            onClickPage = {},\n            onClickEpisode = {}\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun VideoPlayerPartSheetContentPgcSectionsPreview() {\n    val pages = List(10) {\n        VideoPage(\n            cid = it.toLong(),\n            index = it,\n            title = \"Page title $it\",\n            duration = 1,\n            dimension = Dimension(0, 0)\n        )\n    }\n    BVMobileTheme {\n        VideoPlayerPartSheetContent(\n            currentCid = 1,\n            pages = pages,\n            ugcSeason = null,\n            pgcSections = emptyList(),\n            onClickPage = {},\n            onClickEpisode = {}\n        )\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/PreferenceGroup.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.preferences\n\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\nfun LazyListScope.preferenceGroups(\n    vararg groupContents: Pair<String?, PreferenceGroupScope.() -> Unit>,\n    groupSpacing: Dp = 12.dp\n) {\n    var isFirstGroup = true\n    groupContents.forEachIndexed { index, (title, content) ->\n        val scope = PreferenceGroupScope(title, index)\n        scope.content()\n        if (scope.preferences.isNotEmpty()) {\n            if (!isFirstGroup) {\n                item(\n                    key = \"preference_group_spacing_$index\"\n                ) { Spacer(modifier = Modifier.height(groupSpacing)) }\n            }\n            scope.build(this)\n            isFirstGroup = false\n        }\n    }\n}\n\nfun LazyListScope.preferenceGroup(\n    title: String? = null,\n    index: Int = 0,\n    content: PreferenceGroupScope.() -> Unit\n) {\n    val scope = PreferenceGroupScope(title, index)\n    scope.content()\n    if (scope.preferences.isEmpty()) return\n    scope.build(this)\n}\n\n@DslMarker\nannotation class PreferenceGroupScopeMarker\n\n@PreferenceGroupScopeMarker\nclass PreferenceGroupScope internal constructor(\n    private val title: String? = null,\n    private val index: Int\n) {\n    val preferences = mutableListOf<@Composable (shape: Shape, modifier: Modifier) -> Unit>()\n\n    companion object {\n        private val LARGE_CORNER_RADIUS = 16.dp\n        private val SMALL_CORNER_RADIUS = 4.dp\n    }\n\n    internal fun build(listScope: LazyListScope) {\n        if (title != null) {\n            listScope.item(\n                key = \"preference_group_title_${this.index}_${title}\"\n            ) {\n                Text(\n                    text = title,\n                    style = MaterialTheme.typography.labelMedium,\n                    modifier = Modifier\n                        .padding(vertical = 8.dp, horizontal = 4.dp)\n                        .animateItem()\n                )\n            }\n        }\n\n        preferences.forEachIndexed { index, itemContent ->\n            val isFirst = index == 0\n            val isLast = index == preferences.lastIndex\n            val shape = when {\n                isFirst && isLast -> RoundedCornerShape(LARGE_CORNER_RADIUS)\n                isFirst -> RoundedCornerShape(\n                    topStart = LARGE_CORNER_RADIUS,\n                    topEnd = LARGE_CORNER_RADIUS,\n                    bottomStart = SMALL_CORNER_RADIUS,\n                    bottomEnd = SMALL_CORNER_RADIUS\n                )\n\n                isLast -> RoundedCornerShape(\n                    bottomStart = LARGE_CORNER_RADIUS,\n                    bottomEnd = LARGE_CORNER_RADIUS,\n                    topStart = SMALL_CORNER_RADIUS,\n                    topEnd = SMALL_CORNER_RADIUS\n                )\n\n                else -> RoundedCornerShape(SMALL_CORNER_RADIUS)\n            }\n            val modifier = if (!isFirst) Modifier.padding(top = 2.dp) else Modifier\n\n            listScope.item(\n                key = \"preference_group_${this.index}_${title}_${index}\"\n            ) {\n                itemContent(shape, modifier.animateItem())\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/PreferencesPreview.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.preferences\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.PlayCircleOutline\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.mobile.component.preferences.items.switchPreference\nimport dev.aaa1115910.bv.mobile.component.preferences.items.textPreference\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun PreferencesPreview() {\n    BVMobileTheme {\n        var showHiddenPreference by remember { mutableStateOf(false) }\n        Surface(\n            color = MaterialTheme.colorScheme.surfaceContainerLow\n        ) {\n            LazyColumn(\n                modifier = Modifier.padding(12.dp),\n            ) {\n                item {\n                    Text(\n                        modifier = Modifier.animateItem(),\n                        text = \"Preferences Preview\",\n                        style = MaterialTheme.typography.headlineSmall\n                    )\n                }\n                preferenceGroups(\n                    \"Group 1\" to {\n                        textPreference(\n                            title = \"Text Preference\",\n                            summary = \"This is a summary\",\n                        )\n                        textPreference(\n                            title = \"Selected Text Preference\",\n                            summary = \"This is another summary\",\n                            selected = true\n                        )\n                        textPreference(\n                            title = \"No Summary\",\n                        )\n                        textPreference(\n                            title = \"Clickable Text Preference\",\n                            summary = \"This preference is clickable\",\n                            onClick = { /* Handle click */ }\n                        )\n                        textPreference(\n                            title = \"Disabled Text Preference\",\n                            summary = \"This preference is disabled\",\n                            enabled = false\n                        )\n                        textPreference(\n                            title = \"Icon Text Preference\",\n                            summary = \"This preference has an icon\",\n                            icon = Icons.Default.PlayCircleOutline,\n                        )\n                    },\n                    \"Group 2\" to {\n                        textPreference(\n                            title = \"Text Preference\",\n                            summary = \"This is a summary\",\n                        )\n                        switchPreference(\n                            title = \"Switch Preference\",\n                            summary = \"This is a summary\",\n                            leadingContent = {\n                                Icon(\n                                    imageVector = Icons.Default.PlayCircleOutline,\n                                    contentDescription = null\n                                )\n                            },\n                            onClick = { showHiddenPreference = !showHiddenPreference },\n                            checked = showHiddenPreference,\n                            onCheckedChange = { showHiddenPreference = !showHiddenPreference }\n                        )\n                        if (showHiddenPreference) {\n                            textPreference(title = \"Hidden Preference\")\n                        }\n                    },\n                    null to {\n                        textPreference(\n                            title = \"Text Preference\",\n                            summary = \"This is a summary\",\n                        )\n                    }\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/items/BaseListItem.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.preferences.items\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.ListItemColors\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun BaseListItem(\n    headlineContent: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n    overlineContent: @Composable (() -> Unit)? = null,\n    supportingContent: @Composable (() -> Unit)? = null,\n    leadingContent: @Composable (() -> Unit)? = null,\n    trailingContent: @Composable (() -> Unit)? = null,\n    colors: ListItemColors = ListItemDefaults.colors().copy(\n        containerColor = MaterialTheme.colorScheme.surfaceBright,\n        supportingTextColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f),\n    ),\n    tonalElevation: Dp = ListItemDefaults.Elevation,\n    shadowElevation: Dp = ListItemDefaults.Elevation,\n    selected: Boolean = false,\n    enabled: Boolean = true,\n    shape: Shape = MaterialTheme.shapes.medium,\n    onClick: (() -> Unit)? = null,\n) {\n    ListItem(\n        modifier = modifier\n            .clip(shape)\n            .heightIn(min = 72.dp)\n            .clickable(\n                enabled = enabled,\n                onClick = { onClick?.invoke() }\n            ),\n        headlineContent = headlineContent,\n        overlineContent = overlineContent,\n        supportingContent = supportingContent,\n        leadingContent = leadingContent,\n        trailingContent = trailingContent,\n        colors = if (!enabled) colors.copy(\n            //containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.38f),\n            headlineColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),\n            leadingIconColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),\n            overlineColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),\n            supportingTextColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),\n        ) else if (selected) colors.copy(\n            containerColor = MaterialTheme.colorScheme.secondaryContainer\n        ) else colors,\n        tonalElevation = tonalElevation,\n        shadowElevation = shadowElevation\n    )\n}\n\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/items/ListItemPreference.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.preferences.items\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight\nimport androidx.compose.material.icons.filled.PlayCircleOutline\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.ListItemColors\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.mobile.component.preferences.PreferenceGroupScope\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@Composable\nprivate fun ListItemPreference(\n    modifier: Modifier = Modifier,\n    headlineContent: @Composable () -> Unit,\n    overlineContent: @Composable (() -> Unit)? = null,\n    supportingContent: @Composable (() -> Unit)? = null,\n    leadingContent: @Composable (() -> Unit)? = null,\n    trailingContent: @Composable (() -> Unit)? = null,\n    colors: ListItemColors = ListItemDefaults.colors(),\n    shape: Shape = RoundedCornerShape(0.dp),\n    tonalElevation: Dp = ListItemDefaults.Elevation,\n    shadowElevation: Dp = ListItemDefaults.Elevation,\n    onClick: (() -> Unit)? = null\n) {\n    ListItem(\n        modifier = modifier\n            .clip(shape)\n            .clickable { onClick?.invoke() },\n        headlineContent = headlineContent,\n        overlineContent = overlineContent,\n        supportingContent = supportingContent,\n        leadingContent = leadingContent,\n        trailingContent = trailingContent,\n        colors = colors,\n        tonalElevation = tonalElevation,\n        shadowElevation = shadowElevation\n    )\n}\n\nfun PreferenceGroupScope.listItemPreference(\n    headlineContent: @Composable () -> Unit,\n    overlineContent: @Composable (() -> Unit)? = null,\n    supportingContent: @Composable (() -> Unit)? = null,\n    leadingContent: @Composable (() -> Unit)? = null,\n    trailingContent: @Composable (() -> Unit)? = null,\n    colors: ListItemColors? = null,\n    onClick: (() -> Unit)? = null,\n) {\n    preferences += { shape, modifier ->\n        ListItemPreference(\n            modifier = modifier,\n            headlineContent = headlineContent,\n            overlineContent = overlineContent,\n            supportingContent = supportingContent,\n            leadingContent = leadingContent,\n            trailingContent = trailingContent,\n            colors = colors ?: ListItemDefaults.colors(),\n            shape = shape,\n            onClick = onClick\n        )\n    }\n}\n\nfun PreferenceGroupScope.listItemPreference(\n    title: String,\n    summary: String? = null,\n    icon: ImageVector? = null,\n    onClick: (() -> Unit)? = null,\n) = listItemPreference(\n    headlineContent = @Composable { Text(text = title) },\n    supportingContent = if (summary != null) (@Composable { Text(text = summary) }) else null,\n    leadingContent = if (icon != null) (@Composable {\n        Icon(\n            imageVector = icon,\n            contentDescription = null\n        )\n    }) else null,\n    onClick = onClick,\n)\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun ListItemPreferencePreview() {\n    BVMobileTheme {\n        ListItemPreference(\n            headlineContent = { Text(\"Text Preference\") },\n            //supportingContent = { Text(\"This is a summary\") },\n            leadingContent = {\n                Icon(\n                    imageVector = Icons.Default.PlayCircleOutline,\n                    contentDescription = null\n                )\n            },\n            trailingContent = {\n                Icon(\n                    imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,\n                    contentDescription = null\n                )\n            },\n            onClick = { /* Handle click */ },\n            shape = RoundedCornerShape(8.dp),\n        )\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/items/RadioPreference.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.preferences.items\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.PlayCircleOutline\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport de.schnettler.datastore.manager.DataStoreManager\nimport de.schnettler.datastore.manager.PreferenceRequest\nimport dev.aaa1115910.bv.dataStore\nimport dev.aaa1115910.bv.mobile.component.preferences.PreferenceGroupScope\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\n@Composable\nprivate fun <T> RadioPreference(\n    modifier: Modifier = Modifier,\n    title: String,\n    summary: String?,\n    selected: Boolean = false,\n    shape: Shape = RoundedCornerShape(0.dp),\n    enabled: Boolean = true,\n    leadingContent: @Composable() (() -> Unit)? = null,\n    value: T,\n    values: Map<T, String>,\n    onValueChange: ((T) -> Unit)\n) {\n    var showDialog by remember { mutableStateOf(false) }\n\n    BaseListItem(\n        modifier = modifier,\n        headlineContent = { Text(text = title) },\n        supportingContent = { Text(text = summary ?: \"unknown\") },\n        selected = selected,\n        enabled = enabled,\n        leadingContent = leadingContent,\n        onClick = { showDialog = true },\n        shape = shape\n    )\n\n    if (showDialog) {\n        RadioDialog(\n            modifier = Modifier.fillMaxWidth(),\n            title = title,\n            value = value,\n            values = values,\n            onValueChange = { newValue ->\n                onValueChange(newValue)\n                showDialog = false\n            },\n            onDismissRequest = { showDialog = false }\n        )\n    }\n}\n\n@Composable\nprivate fun <T> RadioDialog(\n    modifier: Modifier = Modifier,\n    title: String,\n    value: T,\n    values: Map<T, String>,\n    onValueChange: (T) -> Unit,\n    onDismissRequest: () -> Unit\n) {\n    AlertDialog(\n        modifier = modifier,\n        onDismissRequest = onDismissRequest,\n        title = { Text(text = title) },\n        text = {\n            LazyColumn {\n                items(values.toList()) { (itemValue, label) ->\n                    ListItem(\n                        modifier = Modifier\n                            .clip(MaterialTheme.shapes.medium)\n                            .clickable { onValueChange(itemValue) },\n                        headlineContent = { Text(text = label) },\n                        leadingContent = {\n                            RadioButton(\n                                selected = itemValue == value,\n                                onClick = { onValueChange(itemValue) }\n                            )\n                        },\n                        colors = ListItemDefaults.colors(\n                            containerColor = Color.Transparent,\n                        )\n                    )\n                }\n            }\n        },\n        confirmButton = {},\n    )\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun RadioPreferencePreview() {\n    BVMobileTheme {\n        RadioPreference(\n            title = \"Radio Preference\",\n            summary = \"value\",\n            leadingContent = {\n                Icon(\n                    imageVector = Icons.Default.PlayCircleOutline,\n                    contentDescription = null\n                )\n            },\n            selected = false,\n            shape = RoundedCornerShape(8.dp),\n            enabled = true,\n            value = 123,\n            values = mapOf(\n                123 to \"Option 1\",\n                456 to \"Option 2\",\n                789 to \"Option 3\"\n            ),\n            onValueChange = { /* Handle checked change */ }\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun RadioDialogPreview() {\n    BVMobileTheme {\n        var value by remember { mutableIntStateOf(123) }\n        RadioDialog(\n            title = \"Select Option\",\n            value = value,\n            values = mapOf(\n                123 to \"Option 1\",\n                456 to \"Option 2\",\n                789 to \"Option 3\"\n            ),\n            onValueChange = { value = it },\n            onDismissRequest = { /* Handle dismiss */ }\n        )\n    }\n}\n\nfun <T> PreferenceGroupScope.radioPreference(\n    title: String,\n    leadingContent: @Composable() (() -> Unit)? = null,\n    onSelected: Boolean = false,\n    enabled: Boolean = true,\n    value: T,\n    values: Map<T, String>,\n    onValueChange: (T) -> Unit\n) {\n    preferences += { shape, modifier ->\n        RadioPreference(\n            modifier = modifier,\n            title = title,\n            summary = values[value],\n            leadingContent = leadingContent,\n            selected = onSelected,\n            shape = shape,\n            enabled = enabled,\n            value = value,\n            values = values,\n            onValueChange = onValueChange\n        )\n    }\n}\n\nfun <T> PreferenceGroupScope.radioPreference(\n    title: String,\n    leadingContent: @Composable() (() -> Unit)? = null,\n    onSelected: Boolean = false,\n    enabled: Boolean = true,\n    prefReq: PreferenceRequest<T>,\n    values: Map<T, String>,\n    onValueChange: (T) -> Boolean = { true }\n) {\n    preferences += { shape, modifier ->\n        val scope = rememberCoroutineScope()\n        val dataStoreManager = DataStoreManager(LocalContext.current.dataStore)\n\n        val value by dataStoreManager.getPreferenceState(prefReq)\n        val setValue = { newValue: T ->\n            scope.launch(Dispatchers.IO) {\n                dataStoreManager.editPreference(prefReq.key, newValue)\n            }\n            onValueChange(newValue)\n        }\n\n        RadioPreference(\n            modifier = modifier,\n            title = title,\n            summary = values[value],\n            leadingContent = leadingContent,\n            selected = onSelected,\n            shape = shape,\n            enabled = enabled,\n            value = value,\n            values = values,\n            onValueChange = {\n                if (onValueChange(it)) setValue(it)\n            }\n        )\n    }\n}\n\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/items/SwitchPreference.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.preferences.items\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.PlayCircleOutline\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport de.schnettler.datastore.manager.DataStoreManager\nimport de.schnettler.datastore.manager.PreferenceRequest\nimport dev.aaa1115910.bv.dataStore\nimport dev.aaa1115910.bv.mobile.component.preferences.PreferenceGroupScope\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\n@Composable\nprivate fun SwitchPreference(\n    modifier: Modifier = Modifier,\n    title: String,\n    summary: String? = null,\n    onClick: (() -> Unit)? = null,\n    selected: Boolean = false,\n    shape: Shape = RoundedCornerShape(0.dp),\n    enabled: Boolean = true,\n    leadingContent: @Composable() (() -> Unit)? = null,\n    checked: Boolean,\n    onCheckedChange: ((Boolean) -> Unit)\n) {\n    BaseListItem(\n        modifier = modifier,\n        headlineContent = { Text(text = title) },\n        supportingContent = summary?.let { { Text(text = it) } },\n        selected = selected,\n        enabled = enabled,\n        leadingContent = leadingContent,\n        trailingContent = {\n            Switch(\n                checked = checked,\n                onCheckedChange = onCheckedChange\n            )\n        },\n        onClick = onClick,\n        shape = shape\n    )\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun SwitchPreferencePreview() {\n    BVMobileTheme {\n        SwitchPreference(\n            title = \"Switch Preference\",\n            summary = \"This is a summary\",\n            leadingContent = {\n                Icon(\n                    imageVector = Icons.Default.PlayCircleOutline,\n                    contentDescription = null\n                )\n            },\n            onClick = { /* Handle click */ },\n            selected = false,\n            shape = RoundedCornerShape(8.dp),\n            enabled = true,\n            checked = false,\n            onCheckedChange = { /* Handle checked change */ }\n        )\n    }\n}\n\nfun PreferenceGroupScope.switchPreference(\n    title: String,\n    summary: String? = null,\n    leadingContent: @Composable() (() -> Unit)? = null,\n    onClick: (() -> Unit)? = null,\n    onSelected: Boolean = false,\n    enabled: Boolean = true,\n    checked: Boolean,\n    onCheckedChange: (Boolean) -> Unit\n) {\n    preferences += { shape, modifier ->\n        SwitchPreference(\n            modifier = modifier,\n            title = title,\n            summary = summary,\n            leadingContent = leadingContent,\n            onClick = onClick,\n            selected = onSelected,\n            shape = shape,\n            enabled = enabled,\n            checked = checked,\n            onCheckedChange = onCheckedChange\n        )\n    }\n}\n\nfun PreferenceGroupScope.switchPreference(\n    title: String,\n    summary: String? = null,\n    leadingContent: @Composable() (() -> Unit)? = null,\n    onClick: (() -> Unit)? = null,\n    onSelected: Boolean = false,\n    enabled: Boolean = true,\n    prefReq: PreferenceRequest<Boolean>,\n    onCheckedChange: (Boolean) -> Boolean\n) {\n    preferences += { shape, modifier ->\n        val scope = rememberCoroutineScope()\n        val dataStoreManager = DataStoreManager(LocalContext.current.dataStore)\n\n        val checked by dataStoreManager.getPreferenceState(prefReq)\n        val setChecked = { newValue: Boolean ->\n            scope.launch(Dispatchers.IO) {\n                dataStoreManager.editPreference(prefReq.key, newValue)\n            }\n            onCheckedChange(newValue)\n        }\n\n        SwitchPreference(\n            modifier = modifier,\n            title = title,\n            summary = summary,\n            leadingContent = leadingContent,\n            onClick = onClick,\n            selected = onSelected,\n            shape = shape,\n            enabled = enabled,\n            checked = checked,\n            onCheckedChange = {\n                if (onCheckedChange(it)) setChecked(it)\n            }\n        )\n    }\n}\n\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/preferences/items/TextPreference.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.preferences.items\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight\nimport androidx.compose.material.icons.filled.PlayCircleOutline\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.mobile.component.preferences.PreferenceGroupScope\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@Composable\nprivate fun TextPreference(\n    modifier: Modifier = Modifier,\n    title: String,\n    summary: String? = null,\n    onClick: (() -> Unit)? = null,\n    selected: Boolean = false,\n    shape: Shape = RoundedCornerShape(0.dp),\n    enabled: Boolean = true,\n    leadingContent: @Composable() (() -> Unit)? = null,\n    trailingContent: @Composable() (() -> Unit)? = null,\n) {\n    BaseListItem(\n        modifier = modifier,\n        headlineContent = { Text(text = title) },\n        supportingContent = summary?.let { { Text(text = it) } },\n        selected = selected,\n        enabled = enabled,\n        leadingContent = leadingContent,\n        trailingContent = trailingContent,\n        onClick = onClick,\n        shape = shape\n    )\n}\n\nfun PreferenceGroupScope.textPreference(\n    title: String,\n    summary: String? = null,\n    leadingContent: @Composable() (() -> Unit)? = null,\n    trailingContent: @Composable() (() -> Unit)? = null,\n    onClick: (() -> Unit)? = null,\n    onSelected: Boolean = false,\n    enabled: Boolean = true\n) {\n    preferences += { shape, modifier ->\n        TextPreference(\n            modifier = modifier,\n            title = title,\n            summary = summary,\n            leadingContent = leadingContent,\n            trailingContent = trailingContent,\n            onClick = onClick,\n            selected = onSelected,\n            shape = shape,\n            enabled = enabled\n        )\n    }\n}\n\nfun PreferenceGroupScope.textPreference(\n    title: String,\n    summary: String? = null,\n    icon: ImageVector? = null,\n    onClick: (() -> Unit)? = null,\n    selected: Boolean = false,\n    enabled: Boolean = true\n) = textPreference(\n    title = title,\n    summary = summary,\n    leadingContent = if (icon != null) (@Composable {\n        Icon(\n            imageVector = icon,\n            contentDescription = null\n        )\n    }) else null,\n    onClick = onClick,\n    onSelected = selected,\n    enabled = enabled\n)\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun TextPreferencePreview() {\n    BVMobileTheme {\n        TextPreference(\n            title = \"Text Preference\",\n            summary = \"This is a summary\",\n            leadingContent = {\n                Icon(\n                    imageVector = Icons.Default.PlayCircleOutline,\n                    contentDescription = null\n                )\n            },\n            trailingContent = {\n                Icon(\n                    imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,\n                    contentDescription = null\n                )\n            },\n            onClick = { /* Handle click */ },\n            selected = false,\n            shape = RoundedCornerShape(8.dp),\n            enabled = true\n        )\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/reply/CommentItem.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.reply\n\nimport androidx.compose.animation.core.animateIntAsState\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.CornerSize\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.foundation.text.appendInlineContent\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.Placeholder\nimport androidx.compose.ui.text.PlaceholderVerticalAlign\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.tooling.preview.PreviewParameter\nimport androidx.compose.ui.tooling.preview.PreviewParameterProvider\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport coil.compose.AsyncImage\nimport coil.compose.rememberAsyncImagePainter\nimport com.origeek.imageViewer.previewer.ImagePreviewerState\nimport com.origeek.imageViewer.previewer.TransformImageView\nimport com.origeek.imageViewer.previewer.TransformItemState\nimport com.origeek.imageViewer.previewer.rememberPreviewerState\nimport com.origeek.imageViewer.previewer.rememberTransformItemState\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.biliapi.entity.reply.EmoteSize\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport kotlinx.coroutines.launch\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun CommentItem(\n    modifier: Modifier = Modifier,\n    comment: Comment,\n    previewerState: ImagePreviewerState,\n    showReplies: Boolean = true,\n    containerColor: Color = MaterialTheme.colorScheme.surface,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit,\n    onShowReply: (rpid: Long) -> Unit = {}\n) {\n    Surface(\n        modifier = modifier,\n        color = containerColor\n    ) {\n        Column(\n            modifier = Modifier\n                .padding(vertical = 8.dp)\n                .padding(end = 16.dp),\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Row {\n                AsyncImage(\n                    modifier = Modifier\n                        .padding(horizontal = 16.dp)\n                        .size(40.dp)\n                        .clip(CircleShape)\n                        .background(Color.Gray),\n                    model = comment.member.avatar,\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth(),\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                ) {\n                    Column {\n                        Text(\n                            modifier = Modifier\n                                .width(200.dp)\n                                .basicMarquee(),\n                            text = comment.member.name\n                        )\n                        Text(\n                            text = comment.timeDesc,\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                        )\n                    }\n                    Box {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.spacedBy(4.dp)\n                        ) {\n                            //TODO like comment\n                            /*IconButton(\n                                modifier = Modifier.size(24.dp),\n                                onClick = { *//*TODO*//* }\n                            ) {\n                                Icon(\n                                    modifier = Modifier.size(20.dp),\n                                    imageVector = Icons.Outlined.ThumbUpAlt,\n                                    contentDescription = null\n                                )\n                            }\n                            Text(text = \"233\")*/\n                        }\n                    }\n                }\n            }\n            Column(\n                modifier = Modifier.padding(start = 72.dp),\n                verticalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                CommentText(\n                    content = comment.content,\n                    emotes = comment.emotes\n                )\n                if (comment.pictures.isNotEmpty()) {\n                    CommentPictures(\n                        pictures = comment.pictures,\n                        previewerState = previewerState,\n                        onShowPreviewer = onShowPreviewer\n                    )\n                }\n                if (showReplies && (comment.repliesCount != 0 || comment.replies.isNotEmpty())) {\n                    CommentReplies(\n                        replies = comment.replies,\n                        repliesCount = comment.repliesCount,\n                        onOpenCommentSheet = { onShowReply(comment.rpid) }\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun CommentText(\n    modifier: Modifier = Modifier,\n    content: List<String>,\n    emotes: List<Comment.Emote>,\n    maxLines: Int = 6,\n    showMoreButton: Boolean = true\n) {\n    val emoteNameList = emotes.map { it.text }\n    val inlineContentMap = emotes.map { emote ->\n        emote.text to InlineTextContent(\n            Placeholder(\n                width = emote.size.fontSize.sp,\n                height = emote.size.fontSize.sp,\n                placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter\n            )\n        ) {\n            AsyncImage(model = emote.url, contentDescription = null)\n        }\n    }.toMap()\n\n    var lineCount by remember { mutableIntStateOf(0) }\n    var maxLinesValue by remember { mutableIntStateOf(maxLines) }\n    val currentMaxLines by animateIntAsState(targetValue = maxLinesValue, label = \"text max line\")\n    var textMoreThan6Lines by remember { mutableStateOf(false) }\n\n    Column(\n        modifier = modifier\n    ) {\n        Text(\n            text = buildAnnotatedString {\n                content.forEach { text ->\n                    if (emoteNameList.contains(text)) {\n                        appendInlineContent(text)\n                    } else {\n                        append(text)\n                    }\n                }\n            },\n            inlineContent = inlineContentMap,\n            maxLines = currentMaxLines,\n            overflow = TextOverflow.Ellipsis,\n            onTextLayout = { textLayoutResult ->\n                if (textLayoutResult.hasVisualOverflow) textMoreThan6Lines = true\n                lineCount = textLayoutResult.lineCount\n            }\n        )\n        if (showMoreButton && textMoreThan6Lines) {\n            if (maxLinesValue == maxLines) {\n                Text(\n                    modifier = Modifier.clickable { maxLinesValue = 999 },\n                    text = \"展开\",\n                    color = MaterialTheme.colorScheme.primary,\n                    fontWeight = FontWeight.Bold\n                )\n            } else {\n                Text(\n                    modifier = Modifier.clickable { maxLinesValue = 6 },\n                    text = \"收起\",\n                    color = MaterialTheme.colorScheme.primary,\n                    fontWeight = FontWeight.Bold\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun CommentPictures(\n    modifier: Modifier = Modifier,\n    pictures: List<Picture>,\n    previewerState: ImagePreviewerState,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit,\n) {\n    val scope = rememberCoroutineScope()\n    val imageBaseShape = MaterialTheme.shapes.medium\n\n    val onClickPicture: (index: Int, itemState: TransformItemState) -> Unit = { index, itemState ->\n        onShowPreviewer(pictures) {\n            scope.launch {\n                previewerState.openTransform(\n                    index = index,\n                    itemState = itemState,\n                )\n            }\n        }\n    }\n\n    Box(\n        modifier = modifier\n    ) {\n        when {\n            pictures.size == 1 -> {\n                Row {\n                    val itemState = rememberTransformItemState()\n                    Surface(\n                        modifier = Modifier\n                            .weight(1f)\n                            .aspectRatio(2f),\n                        color = Color.Gray,\n                        shape = imageBaseShape\n                    ) {\n                        TransformImageView(\n                            modifier = Modifier.clickable { onClickPicture(0, itemState) },\n                            painter = rememberAsyncImagePainter(pictures.first().url),\n                            key = pictures.first().key,\n                            itemState = itemState,\n                            previewerState = previewerState,\n                        )\n                    }\n                }\n            }\n\n            pictures.size == 2 -> {\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    pictures.forEachIndexed { index, picture ->\n                        val itemState = rememberTransformItemState()\n                        Surface(\n                            modifier = Modifier\n                                .weight(1f)\n                                .aspectRatio(1f),\n                            color = Color.Gray,\n                            shape = when (index) {\n                                0 -> imageBaseShape.copy(\n                                    topEnd = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)\n                                )\n\n                                1 -> imageBaseShape.copy(\n                                    topStart = CornerSize(0.dp), bottomStart = CornerSize(0.dp)\n                                )\n\n                                else -> RoundedCornerShape(0.dp)\n                            }\n                        ) {\n                            TransformImageView(\n                                modifier = Modifier.clickable { onClickPicture(index, itemState) },\n                                painter = rememberAsyncImagePainter(picture.url),\n                                key = picture.key,\n                                itemState = itemState,\n                                previewerState = previewerState,\n                            )\n                        }\n                    }\n                }\n            }\n\n            pictures.size >= 3 -> {\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    pictures.take(3).forEachIndexed { index, picture ->\n                        val itemState = rememberTransformItemState()\n                        Surface(\n                            modifier = Modifier\n                                .weight(1f)\n                                .aspectRatio(1f),\n                            color = Color.Gray,\n                            shape = when (index) {\n                                0 -> imageBaseShape.copy(\n                                    topEnd = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)\n                                )\n\n                                2 -> imageBaseShape.copy(\n                                    topStart = CornerSize(0.dp), bottomStart = CornerSize(0.dp)\n                                )\n\n                                else -> RoundedCornerShape(0.dp)\n                            }\n                        ) {\n                            TransformImageView(\n                                modifier = Modifier.clickable { onClickPicture(index, itemState) },\n                                painter = rememberAsyncImagePainter(picture.url),\n                                key = picture.key,\n                                itemState = itemState,\n                                previewerState = previewerState,\n                            )\n                        }\n                    }\n                }\n\n                if (pictures.size > 3) {\n                    Text(\n                        modifier = Modifier\n                            .align(Alignment.BottomEnd)\n                            .clip(\n                                MaterialTheme.shapes.medium.copy(\n                                    topEnd = CornerSize(0.dp),\n                                    bottomStart = CornerSize(0.dp)\n                                )\n                            )\n                            .background(Color.Black.copy(alpha = 0.4f))\n                            .padding(horizontal = 8.dp),\n                        text = \"+${pictures.size - 3}\",\n                        color = Color.White\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun CommentReplies(\n    modifier: Modifier = Modifier,\n    replies: List<Comment>,\n    repliesCount: Int,\n    onOpenCommentSheet: () -> Unit\n) {\n    Surface(\n        shape = MaterialTheme.shapes.medium,\n        onClick = onOpenCommentSheet,\n        color = MaterialTheme.colorScheme.surfaceContainer\n    ) {\n        Column(\n            modifier = modifier.padding(8.dp),\n            verticalArrangement = Arrangement.spacedBy(4.dp)\n        ) {\n            replies.forEach { reply ->\n                val replyContent = if (reply.content.firstOrNull()?.startsWith(\"回复\") == true) {\n                    listOf(\"${reply.member.name} \")\n                } else {\n                    listOf(\"${reply.member.name} : \")\n                } + reply.content\n                CommentText(\n                    content = replyContent,\n                    emotes = reply.emotes,\n                    maxLines = 2,\n                    showMoreButton = false\n                )\n            }\n            if (repliesCount > replies.size) {\n                Text(\n                    text = \"共 $repliesCount 条回复\",\n                    style = MaterialTheme.typography.bodySmall\n                )\n            }\n        }\n    }\n}\n\nprivate class CommentItemPreviewParameterProvider :\n    PreviewParameterProvider<Comment> {\n    override val values = sequenceOf(\n        Comment(\n            rpid = 0,\n            mid = 0,\n            oid = 0,\n            parent = 0,\n            type = 0,\n            content = listOf(\"单行文字。你好\", \"[doge]\", \"World!\"),\n            member = Comment.Member(mid = 0, avatar = \"\", name = \"username\"),\n            timeDesc = \"4小时前\",\n            emotes = listOf(\n                Comment.Emote(\n                    text = \"[doge]\",\n                    url = \"https://i0.hdslb.com/bfs/emote/3087d273a78ccaff4bb1e9972e2ba2a7583c9f11.png\",\n                    size = EmoteSize.Small\n                )\n            ),\n            pictures = emptyList(),\n            replies = emptyList(),\n            repliesCount = 0\n        ),\n        Comment(\n            rpid = 0,\n            mid = 0,\n            oid = 0,\n            parent = 0,\n            type = 0,\n            content = listOf(\"超长评论。If you were a web designer in the early days of the Internet, you might remember that there were few “web safe” typefaces, such as Arial and Georgia. As a result, many websites looked similar. To use a new typeface, you had to embed small Flash files for each heading in your layout.\"),\n            member = Comment.Member(\n                mid = 0,\n                avatar = \"\",\n                name = \"超长用户名 超长用户名 超长用户名 超长用户名\"\n            ),\n            timeDesc = \"4小时前\",\n            emotes = listOf(\n                Comment.Emote(\n                    text = \"[doge]\",\n                    url = \"https://i0.hdslb.com/bfs/emote/3087d273a78ccaff4bb1e9972e2ba2a7583c9f11.png\",\n                    size = EmoteSize.Small\n                )\n            ),\n            pictures = emptyList(),\n            replies = emptyList(),\n            repliesCount = 0\n        ),\n        Comment(\n            rpid = 0,\n            mid = 0,\n            oid = 0,\n            parent = 0,\n            type = 0,\n            content = listOf(\"单图片, 1 picture.\"),\n            member = Comment.Member(mid = 0, avatar = \"\", name = \"username\"),\n            timeDesc = \"4小时前\",\n            emotes = emptyList(),\n            pictures = listOf(\n                Picture(\n                    url = \"\",\n                    width = 0,\n                    height = 0,\n                    key = \"\"\n                )\n            ),\n            replies = emptyList(),\n            repliesCount = 0\n        ),\n        Comment(\n            rpid = 0,\n            mid = 0,\n            oid = 0,\n            parent = 0,\n            type = 0,\n            content = listOf(\"双图片, 2 pictures.\"),\n            member = Comment.Member(mid = 0, avatar = \"\", name = \"username\"),\n            timeDesc = \"4小时前\",\n            emotes = emptyList(),\n            pictures = listOf(\n                Picture(url = \"\", width = 0, height = 0, key = \"1\"),\n                Picture(url = \"\", width = 0, height = 0, key = \"2\")\n            ),\n            replies = emptyList(),\n            repliesCount = 0\n        ),\n        Comment(\n            rpid = 0,\n            mid = 0,\n            oid = 0,\n            parent = 0,\n            type = 0,\n            content = listOf(\"三图片, 3 pictures.\"),\n            member = Comment.Member(mid = 0, avatar = \"\", name = \"username\"),\n            timeDesc = \"4小时前\",\n            emotes = emptyList(),\n            pictures = listOf(\n                Picture(url = \"\", width = 0, height = 0, key = \"1\"),\n                Picture(url = \"\", width = 0, height = 0, key = \"2\"),\n                Picture(url = \"\", width = 0, height = 0, key = \"3\")\n            ),\n            replies = emptyList(),\n            repliesCount = 0\n        ),\n        Comment(\n            rpid = 0,\n            mid = 0,\n            oid = 0,\n            parent = 0,\n            type = 0,\n            content = listOf(\"四图片, four pictures.\"),\n            member = Comment.Member(mid = 0, avatar = \"\", name = \"username\"),\n            timeDesc = \"4小时前\",\n            emotes = emptyList(),\n            pictures = listOf(\n                Picture(url = \"\", width = 0, height = 0, key = \"1\"),\n                Picture(url = \"\", width = 0, height = 0, key = \"2\"),\n                Picture(url = \"\", width = 0, height = 0, key = \"3\"),\n                Picture(url = \"\", width = 0, height = 0, key = \"4\")\n            ),\n            replies = emptyList(),\n            repliesCount = 0\n        ),\n        Comment(\n            rpid = 0,\n            mid = 0,\n            oid = 0,\n            parent = 0,\n            type = 0,\n            content = listOf(\"先兼容后慢慢过渡到完全自主，虽然看起来像安卓套壳，但能避免跨度太大扯到蛋。\"),\n            member = Comment.Member(mid = 0, avatar = \"\", name = \"username\"),\n            timeDesc = \"4小时前\",\n            emotes = emptyList(),\n            pictures = listOf(\n                Picture(url = \"\", width = 0, height = 0, key = \"1\"),\n                Picture(url = \"\", width = 0, height = 0, key = \"2\"),\n                Picture(url = \"\", width = 0, height = 0, key = \"3\"),\n                Picture(url = \"\", width = 0, height = 0, key = \"4\")\n            ),\n            replies = listOf(\n                Comment(\n                    rpid = 0,\n                    mid = 0,\n                    oid = 0,\n                    parent = 0,\n                    type = 0,\n                    content = listOf(\"其他视频的置顶：美国商务部的源文件里写的很清楚，对于消费用途的产品（consumer application）是exemption(豁免)。但是基于AD102的产品不得在中国大陆生产，也就是说未来国内销售的RTX 4090将会是在境外生产再运输回国内卖，这是唯一的不同点。估计后续也会是商家炒作显卡涨价的理由。\"),\n                    member = Comment.Member(mid = 0, avatar = \"\", name = \"余Mercury\"),\n                    timeDesc = \"4小时前\",\n                    emotes = emptyList(),\n                    pictures = emptyList(),\n                    replies = emptyList(),\n                    repliesCount = 0\n                ),\n                Comment(\n                    rpid = 0,\n                    mid = 0,\n                    oid = 0,\n                    parent = 0,\n                    type = 0,\n                    content = listOf(\"回复 @余Mercury : 中东佬禁酒,用的泡沫水\"),\n                    member = Comment.Member(mid = 0, avatar = \"\", name = \"铭轩-T\"),\n                    timeDesc = \"4小时前\",\n                    emotes = emptyList(),\n                    pictures = emptyList(),\n                    replies = emptyList(),\n                    repliesCount = 0\n                ),\n                Comment(\n                    rpid = 0,\n                    mid = 0,\n                    oid = 0,\n                    parent = 0,\n                    type = 0,\n                    content = listOf(\"澄清完更好笑了\"),\n                    member = Comment.Member(mid = 0, avatar = \"\", name = \"Gemini好辣辣\"),\n                    timeDesc = \"4小时前\",\n                    emotes = emptyList(),\n                    pictures = emptyList(),\n                    replies = emptyList(),\n                    repliesCount = 0\n                )\n            ),\n            repliesCount = 0\n        )\n    )\n}\n\n@Preview\n@Composable\nprivate fun CommentItemPreview(\n    @PreviewParameter(CommentItemPreviewParameterProvider::class) comment: Comment\n) {\n    val previewerState = rememberPreviewerState(pageCount = { 0 })\n    BVMobileTheme {\n        CommentItem(\n            comment = comment,\n            previewerState = previewerState,\n            onShowPreviewer = { _, _ -> }\n        )\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/reply/Comments.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.reply\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.origeek.imageViewer.previewer.ImagePreviewerState\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.biliapi.entity.reply.CommentSort\n\n@OptIn(\n    ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,\n    ExperimentalMaterial3ExpressiveApi::class\n)\n@Composable\nfun Comments(\n    modifier: Modifier = Modifier,\n    previewerState: ImagePreviewerState,\n    header: (@Composable () -> Unit)? = null,\n    comments: List<Comment>,\n    commentSort: CommentSort,\n    isLoading: Boolean,\n    isRefreshing: Boolean,\n    onLoadMoreComments: () -> Unit,\n    onRefreshComments: () -> Unit,\n    onSwitchCommentSort: (CommentSort) -> Unit,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit,\n    onShowReplies: (comment: Comment) -> Unit\n) {\n    val listState = rememberLazyListState()\n    val pullToRefreshState = rememberPullToRefreshState()\n\n    val shouldLoadMore by remember {\n        derivedStateOf {\n            val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()\n                ?: return@derivedStateOf true\n\n            lastVisibleItem.index >= listState.layoutInfo.totalItemsCount - 10\n        }\n    }\n\n    LaunchedEffect(shouldLoadMore) {\n        if (shouldLoadMore) onLoadMoreComments()\n    }\n\n    PullToRefreshBox(\n        isRefreshing = isRefreshing,\n        state = pullToRefreshState,\n        onRefresh = onRefreshComments\n    ) {\n        LazyColumn(\n            modifier = modifier.fillMaxSize(),\n            state = listState\n        ) {\n            item {\n                header?.invoke()\n            }\n            stickyHeader {\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .background(MaterialTheme.colorScheme.surface)\n                        .padding(start = 16.dp, end = 8.dp),\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Text(\n                        text = when (commentSort) {\n                            CommentSort.Hot -> \"热门评论\"\n                            CommentSort.Time -> \"最新评论\"\n                            else -> \"\"\n                        },\n                        style = MaterialTheme.typography.titleMedium\n                    )\n                    TextButton(onClick = {\n                        onSwitchCommentSort(\n                            when (commentSort) {\n                                CommentSort.Hot -> CommentSort.Time\n                                CommentSort.Time -> CommentSort.Hot\n                                else -> CommentSort.Hot\n                            }\n                        )\n                    }) {\n                        Text(\n                            text = when (commentSort) {\n                                CommentSort.Hot -> \"按热度\"\n                                CommentSort.Time -> \"按时间\"\n                                else -> \"\"\n                            }\n                        )\n                    }\n                }\n            }\n\n            itemsIndexed(items = comments) { index, comment ->\n                Box {\n                    CommentItem(\n                        comment = comment,\n                        previewerState = previewerState,\n                        onShowPreviewer = onShowPreviewer,\n                        onShowReply = { _ ->\n                            onShowReplies(comment)\n                        }\n                    )\n                }\n            }\n\n            if (comments.isEmpty() && !(isLoading || isRefreshing)) {\n                item {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .height(300.dp),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        Text(text = \"啥都没有\")\n                    }\n                }\n            }\n\n            if (isLoading) {\n                item {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .height(100.dp),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        LoadingIndicator()\n                    }\n                }\n            }\n\n            item {\n                Spacer(modifier = Modifier.navigationBarsPadding())\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/reply/Replies.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.reply\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.origeek.imageViewer.previewer.ImagePreviewerState\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.biliapi.entity.reply.CommentSort\nimport dev.aaa1115910.bv.BuildConfig\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun Replies(\n    modifier: Modifier = Modifier,\n    previewerState: ImagePreviewerState,\n    rootComment: Comment?,\n    replies: List<Comment>,\n    replySort: CommentSort,\n    repliesCount: Int,\n    isLoading: Boolean,\n    isRefreshing: Boolean,\n    onLoadMoreReplies: () -> Unit,\n    onRefreshReplies: () -> Unit,\n    onSwitchReplySort: (CommentSort) -> Unit,\n    onShowPreviewer: (List<Picture>, () -> Unit) -> Unit,\n) {\n    val listState = rememberLazyListState()\n    val pullToRefreshState = rememberPullToRefreshState()\n\n    val shouldLoadMore by remember {\n        derivedStateOf {\n            val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()\n                ?: return@derivedStateOf true\n\n            lastVisibleItem.index >= listState.layoutInfo.totalItemsCount - 10\n        }\n    }\n\n    LaunchedEffect(shouldLoadMore) {\n        if (shouldLoadMore) onLoadMoreReplies()\n    }\n\n    PullToRefreshBox(\n        modifier = modifier,\n        state = pullToRefreshState,\n        isRefreshing = isRefreshing,\n        onRefresh = onRefreshReplies,\n    ) {\n        LazyColumn(\n            modifier = Modifier.fillMaxHeight(),\n            state = listState\n        ) {\n            if (rootComment != null) {\n                item {\n                    CommentItem(\n                        comment = rootComment,\n                        previewerState = previewerState,\n                        onShowPreviewer = onShowPreviewer,\n                        showReplies = false\n                    )\n                }\n            }\n\n            item {\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(start = 16.dp, end = 8.dp),\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Text(\n                        text = \"相关回复共 $repliesCount 条\",\n                        style = MaterialTheme.typography.titleMedium\n                    )\n                    TextButton(onClick = {\n                        onSwitchReplySort(\n                            when (replySort) {\n                                CommentSort.Hot -> CommentSort.Time\n                                CommentSort.Time -> CommentSort.Hot\n                                else -> CommentSort.Hot\n                            }\n                        )\n                    }) {\n                        Text(\n                            text = when (replySort) {\n                                CommentSort.Hot -> \"按热度\"\n                                CommentSort.Time -> \"按时间\"\n                                else -> \"\"\n                            }\n                        )\n                    }\n                }\n            }\n\n            itemsIndexed(items = replies) { index, reply ->\n                Box {\n                    CommentItem(\n                        comment = reply,\n                        previewerState = previewerState,\n                        onShowPreviewer = onShowPreviewer,\n                        showReplies = false\n                    )\n\n                    if (BuildConfig.DEBUG) {\n                        Text(text = \"$index\")\n                    }\n                }\n            }\n\n            if (replies.isEmpty() && !(isLoading || isRefreshing)) {\n                item {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .height(300.dp),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        Text(text = \"啥都没有\")\n                    }\n                }\n            }\n\n            if (isLoading) {\n                item {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .height(100.dp),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        LoadingIndicator()\n                    }\n                }\n            }\n\n            item {\n                Spacer(modifier = Modifier.navigationBarsPadding())\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/reply/ReplySheetScaffold.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.reply\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.CornerSize\nimport androidx.compose.material3.BottomSheetScaffold\nimport androidx.compose.material3.BottomSheetScaffoldState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SheetValue\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.origeek.imageViewer.previewer.ImagePreviewerState\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.biliapi.entity.reply.CommentReplyPage\nimport dev.aaa1115910.biliapi.entity.reply.CommentSort\nimport dev.aaa1115910.biliapi.repositories.VideoDetailRepository\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.util.OnBottomReached\nimport dev.aaa1115910.bv.util.Prefs\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.compose.getKoin\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ReplySheetScaffold(\n    modifier: Modifier = Modifier,\n    aid: Long,\n    rpid: Long,\n    repliesCount: Int,\n    sheetState: BottomSheetScaffoldState,\n    previewerState: ImagePreviewerState,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit,\n    videoDetailRepository: VideoDetailRepository = getKoin().get(),\n    content: @Composable () -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val listState = rememberLazyListState()\n    val logger = KotlinLogging.logger(\"ReplySheetScaffold\")\n\n    var nextPage by remember { mutableStateOf(CommentReplyPage()) }\n    var comment by remember { mutableStateOf<Comment?>(null) }\n    val replies = remember { mutableStateListOf<Comment>() }\n    var sort by remember { mutableStateOf(CommentSort.Time) }\n    var hasNext by remember { mutableStateOf(true) }\n    var loading by remember { mutableStateOf(false) }\n\n    val loadMoreReply = {\n        if (hasNext && !loading) {\n            loading = true\n            logger.info { \"load more reply: [aid=$aid, rpid=$rpid, next=$nextPage]\" }\n            scope.launch(Dispatchers.IO) {\n                runCatching {\n                    val commentRepliesData = videoDetailRepository.getCommentReplies(\n                        aid = aid,\n                        commentId = rpid,\n                        page = nextPage,\n                        sort = sort,\n                        preferApiType = Prefs.apiType\n                    )\n                    hasNext = commentRepliesData.hasNext\n                    nextPage = commentRepliesData.nextPage\n                    if (comment == null) comment = commentRepliesData.rootComment\n                    replies.addAll(commentRepliesData.replies)\n                }.onFailure {\n                    it.printStackTrace()\n                    hasNext = false\n                }\n                loading = false\n            }\n        }\n    }\n\n    val clearData = {\n        hasNext = true\n        comment = null\n        replies.clear()\n        nextPage = CommentReplyPage()\n    }\n\n    val sheetExpanded by remember {\n        derivedStateOf {\n            sheetState.bottomSheetState.currentValue == SheetValue.Expanded\n        }\n    }\n\n    if (sheetExpanded) {\n        listState.OnBottomReached(loading = loading) {\n            loadMoreReply()\n        }\n    }\n\n    val switchCommentSort: (CommentSort) -> Unit = { newSort ->\n        sort = newSort\n        clearData()\n        loadMoreReply()\n    }\n\n    LaunchedEffect(rpid) {\n        clearData()\n    }\n\n    LaunchedEffect(sheetState.bottomSheetState.currentValue) {\n        when (sheetState.bottomSheetState.currentValue) {\n            SheetValue.Hidden, SheetValue.PartiallyExpanded -> clearData()\n            SheetValue.Expanded -> {}//loadMoreReply()\n        }\n    }\n\n    BackHandler(\n        sheetState.bottomSheetState.currentValue != SheetValue.PartiallyExpanded\n                && !(previewerState.canClose || previewerState.animating)\n    ) {\n        scope.launch { sheetState.bottomSheetState.partialExpand() }\n    }\n\n    BottomSheetScaffold(\n        modifier = modifier\n            .background(MaterialTheme.colorScheme.surfaceContainer)\n            .clip(\n                MaterialTheme.shapes.extraLarge.copy(\n                    bottomStart = CornerSize(0.dp),\n                    bottomEnd = CornerSize(0.dp)\n                )\n            ),\n        scaffoldState = sheetState,\n        sheetPeekHeight = 0.dp,\n        sheetContent = {\n            LazyColumn(\n                modifier = Modifier.fillMaxHeight(),\n                state = listState\n            ) {\n                if (comment != null) {\n                    item {\n                        CommentItem(\n                            comment = comment!!,\n                            previewerState = previewerState,\n                            onShowPreviewer = onShowPreviewer,\n                            showReplies = false,\n                            containerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                        )\n                    }\n                }\n\n                item {\n                    Row(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(start = 16.dp, end = 8.dp),\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Text(\n                            text = \"相关回复共 $repliesCount 条\",\n                            style = MaterialTheme.typography.titleMedium\n                        )\n                        TextButton(onClick = {\n                            switchCommentSort(\n                                when (sort) {\n                                    CommentSort.Hot -> CommentSort.Time\n                                    CommentSort.Time -> CommentSort.Hot\n                                    else -> CommentSort.Hot\n                                }\n                            )\n                        }) {\n                            Text(\n                                text = when (sort) {\n                                    CommentSort.Hot -> \"按热度\"\n                                    CommentSort.Time -> \"按时间\"\n                                    else -> \"\"\n                                }\n                            )\n                        }\n                    }\n                }\n\n                itemsIndexed(items = replies) { index, reply ->\n                    Box {\n                        CommentItem(\n                            comment = reply,\n                            previewerState = previewerState,\n                            onShowPreviewer = onShowPreviewer,\n                            showReplies = false,\n                            containerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                        )\n\n                        if (BuildConfig.DEBUG) {\n                            Text(text = \"$index\")\n                        }\n                    }\n                }\n\n                item {\n                    Spacer(modifier = Modifier.navigationBarsPadding())\n                }\n            }\n        }\n    ) {\n        content()\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/search/PgcCard.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.search\n\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/search/UgcCard.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.search\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.ugc.toSmartDate\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.mobile.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.mobile.component.videocard.UpIcon\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.resizedImageUrl\n\n@Composable\nfun UgcCard(\n    modifier: Modifier = Modifier,\n    data: VideoCardData,\n    onClick: () -> Unit = {}\n) = SmallVideoCard(\n    modifier = modifier,\n    data = data,\n    onClick = onClick\n)\n\n@Composable\nfun UgcListItem(\n    modifier: Modifier = Modifier,\n    data: VideoCardData,\n    onClick: () -> Unit = {}\n) {\n    Surface(\n        onClick = onClick\n    ) {\n        Row(\n            modifier = modifier\n                .fillMaxWidth()\n                .height(94.dp)\n                .padding(4.dp),\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Box {\n                AsyncImage(\n                    modifier = Modifier\n                        .fillMaxHeight()\n                        .aspectRatio(1.8f)\n                        .clip(MaterialTheme.shapes.small)\n                        .background(MaterialTheme.colorScheme.surfaceVariant),\n                    model = data.cover.resizedImageUrl(ImageSize.SmallVideoCardCover),\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n                Text(\n                    modifier = Modifier\n                        .align(Alignment.BottomEnd)\n                        .padding(4.dp)\n                        .clip(MaterialTheme.shapes.extraSmall)\n                        .background(Color.Black.copy(alpha = 0.6f))\n                        .padding(horizontal = 2.dp, vertical = 0.dp),\n                    text = data.timeString,\n                    color = Color.White,\n                    style = MaterialTheme.typography.bodySmall\n                )\n            }\n\n            Column(\n                modifier = Modifier.fillMaxHeight(),\n                verticalArrangement = Arrangement.SpaceBetween\n            ) {\n                Text(\n                    text = data.title,\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis,\n                    style = MaterialTheme.typography.titleSmall\n                )\n                CompositionLocalProvider(\n                    LocalTextStyle provides MaterialTheme.typography.bodySmall\n                ) {\n                    Column {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            UpIcon(modifier = Modifier.size(16.dp))\n                            Text(text = \"bishi\")\n                        }\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.spacedBy(8.dp)\n                        ) {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.spacedBy(2.dp)\n                            ) {\n                                Icon(\n                                    modifier = Modifier,\n                                    painter = painterResource(id = R.drawable.ic_play_count),\n                                    contentDescription = null,\n                                )\n                                Text(text = data.playString)\n                            }\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.spacedBy(2.dp)\n                            ) {\n                                Icon(\n                                    modifier = Modifier,\n                                    painter = painterResource(id = R.drawable.ic_danmaku_count),\n                                    contentDescription = null,\n                                )\n                                Text(text = data.danmakuString)\n                            }\n                            data.pubTime?.toLong()?.toSmartDate()?.let { Text(text = it) }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun UgcListItemPreview() {\n    BVMobileTheme {\n        UgcListItem(\n            data = previewData\n        )\n    }\n}\n\nprivate val previewData = VideoCardData(\n    avid = 0,\n    title = \"震惊！太震惊了！真的是太震惊了！我的天呐！真TMD震惊！\",\n    cover = \"http://i2.hdslb.com/bfs/archive/af17fc07b8f735e822563cc45b7b5607a491dfff.jpg\",\n    upName = \"bishi\",\n    play = 2333,\n    danmaku = 66666,\n    time = 2333 * 1000,\n    pubTime = \"3小时前\"\n)"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/search/UserCard.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.search\n\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/settings/UpdateDialog.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.settings\n\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport dev.aaa1115910.bv.component.settings.UpdateDialog\n\n@Composable\nfun UpdateDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit\n) {\n    UpdateDialog(\n        modifier = modifier,\n        show = show,\n        onHideDialog = onHideDialog,\n        text = { text ->\n            Text(text = text)\n        },\n        button = { enabled, onClick, content ->\n            Button(\n                enabled = enabled,\n                onClick = onClick,\n                content = content\n            )\n        },\n        outlinedButton = { enabled, onClick, content ->\n            OutlinedButton(\n                enabled = enabled,\n                onClick = onClick,\n                content = content\n            )\n        }\n    )\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/user/UserAvatar.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.user\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@Composable\nfun UserAvatar(\n    modifier: Modifier = Modifier,\n    size: Dp =80.dp,\n    avatar: String\n) {\n    Surface(\n        modifier = modifier.size(size),\n        shape = CircleShape,\n        color = MaterialTheme.colorScheme.surfaceVariant,\n        onClick = {}\n    ) {\n        AsyncImage(\n            modifier = Modifier\n                .size(80.dp)\n                .clip(CircleShape),\n            model = avatar,\n            contentDescription = null,\n            contentScale = ContentScale.FillBounds\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun UserAvatarPreview() {\n    BVMobileTheme {\n        Surface {\n            UserAvatar(\n                avatar = \"https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg\"\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/videocard/RelatedVideoItem.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.videocard\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.user.Author\nimport dev.aaa1115910.biliapi.entity.video.RelatedVideo\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.formatHourMinSec\n\n@Composable\nfun RelatedVideoItem(\n    modifier: Modifier = Modifier,\n    relatedVideo: RelatedVideo,\n    onClick: (RelatedVideo) -> Unit = {}\n) {\n    Surface(\n        modifier = modifier,\n        onClick = { onClick(relatedVideo) },\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .height(97.dp)\n                .padding(8.dp),\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Box {\n                AsyncImage(\n                    modifier = Modifier\n                        .fillMaxHeight()\n                        .aspectRatio(16 / 9f)\n                        .background(Color.Gray, MaterialTheme.shapes.small)\n                        .clip(MaterialTheme.shapes.small),\n                    model = relatedVideo.cover,\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n                Surface(\n                    modifier = Modifier\n                        .align(Alignment.BottomEnd)\n                        .padding(4.dp),\n                    color = Color.Black.copy(alpha = 0.3f),\n                    shape = MaterialTheme.shapes.extraSmall\n                ) {\n                    Text(\n                        modifier = Modifier\n                            .padding(horizontal = 4.dp),\n                        text = (relatedVideo.duration * 1000L).formatHourMinSec(),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White\n                    )\n                }\n            }\n\n            Column(\n                modifier = Modifier.fillMaxHeight(),\n                verticalArrangement = Arrangement.SpaceBetween\n            ) {\n                Text(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = relatedVideo.title,\n                    //style = MaterialTheme.typography.titleMedium,\n                    minLines = 2,\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis\n                )\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(2.dp)\n                ) {\n                    Icon(\n                        modifier = Modifier.size(16.dp),\n                        painter = painterResource(id = R.drawable.ic_up),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                    )\n                    Text(\n                        text = \"${relatedVideo.author?.name}\",\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                    )\n                }\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(2.dp)\n                    ) {\n                        Icon(\n                            painter = painterResource(id = R.drawable.ic_play_count),\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                        )\n                        Text(\n                            text = \"${relatedVideo.view}\",\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                        )\n                    }\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(2.dp)\n                    ) {\n                        Icon(\n                            painter = painterResource(id = R.drawable.ic_danmaku_count),\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                        )\n                        Text(\n                            text = \"${relatedVideo.danmaku}\",\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                        )\n                    }\n\n                }\n            }\n        }\n    }\n\n}\n\n@Preview\n@Composable\nfun RelatedVideoItemPreview() {\n    BVMobileTheme {\n        Surface {\n            RelatedVideoItem(\n                relatedVideo = RelatedVideo(\n                    aid = 0,\n                    title = \"This is a video title! This is a video title! This is a video title! \",\n                    cover = \"\",\n                    author = Author(\n                        mid = 0,\n                        name = \"Up name\",\n                        face = \"\",\n                    ),\n                    duration = 5346,\n                    view = 3521,\n                    danmaku = 543,\n                    jumpToSeason = false,\n                    epid = null\n                )\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/videocard/SeasonCard.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.videocard\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.entity.carddata.SeasonCardData\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@Composable\nfun SeasonCard(\n    modifier: Modifier = Modifier,\n    data: SeasonCardData,\n    coverHeight: Dp? = null,\n    onClick: () -> Unit = {},\n) {\n    val localDensity = LocalDensity.current\n    var coverRealWidth by remember { mutableStateOf(0.dp) }\n\n    Card(\n        modifier = modifier,\n        onClick = onClick,\n        shape = MaterialTheme.shapes.medium,\n        colors = CardDefaults.cardColors(\n            containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n        )\n    ) {\n        Column {\n            val coverModifier = if (coverHeight != null) {\n                Modifier.height(coverHeight)\n            } else {\n                Modifier.fillMaxWidth()\n            }\n            val textBoxModifier = if (coverHeight != null) {\n                Modifier.width((0.75 * coverHeight.value).dp)\n            } else {\n                Modifier\n            }\n\n            Box(\n                modifier = Modifier.clip(MaterialTheme.shapes.medium),\n                contentAlignment = Alignment.BottomCenter\n            ) {\n                AsyncImage(\n                    modifier = coverModifier\n                        .aspectRatio(0.75f)\n                        .clip(MaterialTheme.shapes.medium)\n                        .onGloballyPositioned { coordinates ->\n                            coverRealWidth = with(localDensity) { coordinates.size.width.toDp() }\n                        },\n                    model = data.cover,\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n\n                if (data.rating != null) {\n                    Box(\n                        modifier = Modifier\n                            .height(48.dp)\n                            // 无法使用 fillMaxWidth 来确定宽度\n                            .width(coverRealWidth)\n                            .background(\n                                Brush.verticalGradient(\n                                    colors = listOf(\n                                        Color.Transparent,\n                                        Color.Black.copy(alpha = 0.8f)\n                                    )\n                                )\n                            )\n                    )\n                    Text(\n                        modifier = Modifier\n                            .align(Alignment.BottomEnd)\n                            .fillMaxWidth()\n                            .padding(8.dp, 0.dp),\n                        text = data.rating ?: \"\",\n                        fontStyle = FontStyle.Italic,\n                        fontWeight = FontWeight.Bold,\n                        fontSize = 24.sp,\n                        textAlign = TextAlign.End,\n                        color = Color.White\n                    )\n                }\n            }\n\n            Column(\n                modifier = textBoxModifier.padding(8.dp)\n            ) {\n                Text(\n                    text = data.title,\n                    style = MaterialTheme.typography.titleMedium,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n                if (data.subTitle != null) {\n                    Text(\n                        text = data.subTitle!!,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        fontSize = 12.sp,\n                        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun SeasonCardPreview() {\n    BVMobileTheme {\n        LazyVerticalGrid(columns = GridCells.Fixed(6)) {\n            repeat(6) {\n                item {\n                    SeasonCard(\n                        data = SeasonCardData(\n                            seasonId = 40794,\n                            title = \"007：没空去死\",\n                            cover = \"http://i0.hdslb.com/bfs/bangumi/image/8d211c396aad084d6fa413015200dda6ed260768.png\",\n                            rating = \"8.6\"\n                        )\n                    )\n                }\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/videocard/SmallVideoCard.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.videocard\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.resizedImageUrl\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SmallVideoCard(\n    modifier: Modifier = Modifier,\n    data: VideoCardData,\n    onClick: () -> Unit = {},\n) {\n    Card(\n        modifier = modifier,\n        onClick = onClick,\n        shape = MaterialTheme.shapes.medium,\n        colors = CardDefaults.cardColors(\n            containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n        )\n    ) {\n        Column {\n            Box(\n                modifier = Modifier.clip(MaterialTheme.shapes.medium),\n                contentAlignment = Alignment.BottomCenter\n            ) {\n                AsyncImage(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .aspectRatio(1.6f)\n                        .clip(MaterialTheme.shapes.medium),\n                    model = data.cover.resizedImageUrl(ImageSize.SmallVideoCardCover),\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(48.dp)\n                        .background(\n                            Brush.verticalGradient(\n                                colors = listOf(\n                                    Color.Transparent,\n                                    Color.Black.copy(alpha = 0.8f)\n                                )\n                            )\n                        )\n                )\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(12.dp, 8.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        if (data.playString != \"\") {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.spacedBy(2.dp)\n                            ) {\n                                Icon(\n                                    modifier = Modifier,\n                                    painter = painterResource(id = R.drawable.ic_play_count),\n                                    contentDescription = null,\n                                    tint = Color.White\n                                )\n                                Text(\n                                    text = data.playString,\n                                    style = MaterialTheme.typography.bodySmall,\n                                    color = Color.White\n                                )\n                            }\n                        }\n                        if (data.danmakuString != \"\") {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.spacedBy(2.dp)\n                            ) {\n                                Icon(\n                                    modifier = Modifier,\n                                    painter = painterResource(id = R.drawable.ic_danmaku_count),\n                                    contentDescription = null,\n                                    tint = Color.White\n                                )\n                                Text(\n                                    text = data.danmakuString,\n                                    style = MaterialTheme.typography.bodySmall,\n                                    color = Color.White\n                                )\n                            }\n                        }\n                    }\n                    Text(\n                        text = data.timeString,\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White\n                    )\n                }\n            }\n            Column(\n                modifier = Modifier.padding(8.dp)\n            ) {\n                Text(\n                    text = data.title,\n                    style = MaterialTheme.typography.titleSmall,\n                    maxLines = 2,\n                    minLines = 2,\n                    overflow = TextOverflow.Ellipsis\n                )\n                Row(\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    UpIcon()\n                    Text(\n                        text = data.upName,\n                        style = MaterialTheme.typography.labelMedium,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nfun SmallVideoCardPreview() {\n    val data = VideoCardData(\n        avid = 0,\n        title = \"震惊！太震惊了！真的是太震惊了！我的天呐！真TMD震惊！\",\n        cover = \"http://i2.hdslb.com/bfs/archive/af17fc07b8f735e822563cc45b7b5607a491dfff.jpg\",\n        upName = \"bishi\",\n        play = 2333,\n        danmaku = 666,\n        time = 2333 * 1000\n    )\n    BVMobileTheme {\n        Surface {\n            SmallVideoCard(\n                data = data\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nfun SmallVideoCardsPreview() {\n    val data = VideoCardData(\n        avid = 0,\n        title = \"震惊！太震惊了！真的是太震惊了！我的天呐！真TMD震惊！\",\n        cover = \"http://i2.hdslb.com/bfs/archive/af17fc07b8f735e822563cc45b7b5607a491dfff.jpg\",\n        upName = \"bishi\",\n        play = 2333,\n        danmaku = 666,\n        time = 2333 * 1000\n    )\n    BVMobileTheme {\n        Surface {\n            LazyVerticalGrid(\n                columns = GridCells.Fixed(2)\n            ) {\n                repeat(20) {\n                    item {\n                        SmallVideoCard(\n                            data = data\n                        )\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/videocard/UpIcon.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.videocard\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@Composable\nfun UpIcon(\n    modifier: Modifier = Modifier,\n    //color: Color = MaterialTheme.colorScheme.onSurface\n) {\n    Icon(\n        modifier = modifier,\n        painter = painterResource(id = R.drawable.ic_up),\n        contentDescription = null,\n        //tint = color\n    )\n}\n\n@Preview\n@Composable\nfun UpIconPreview() {\n    BVMobileTheme {\n        Row(\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            UpIcon()\n            Text(text = \"bishi\")\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/component/videocard/UpSpaceVideoItem.kt",
    "content": "package dev.aaa1115910.bv.mobile.component.videocard\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.user.SpaceVideo\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport dev.aaa1115910.bv.util.formatPubTimeString\nimport java.util.Date\n\n@Composable\nfun UpSpaceVideoItem(\n    modifier: Modifier = Modifier,\n    spaceVideo: SpaceVideo,\n    onClick: (SpaceVideo) -> Unit = {}\n) {\n    val context = LocalContext.current\n\n    Surface(\n        modifier = modifier,\n        onClick = { onClick(spaceVideo) },\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .height(97.dp)\n                .padding(8.dp),\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Box {\n                AsyncImage(\n                    modifier = Modifier\n                        .fillMaxHeight()\n                        .aspectRatio(16 / 9f)\n                        .background(Color.Gray, MaterialTheme.shapes.small)\n                        .clip(MaterialTheme.shapes.small),\n                    model = spaceVideo.cover,\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n                Surface(\n                    modifier = Modifier\n                        .align(Alignment.BottomEnd)\n                        .padding(4.dp),\n                    color = Color.Black.copy(alpha = 0.3f),\n                    shape = MaterialTheme.shapes.extraSmall\n                ) {\n                    Text(\n                        modifier = Modifier\n                            .padding(horizontal = 4.dp),\n                        text = (spaceVideo.duration * 1000L).formatHourMinSec(),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White\n                    )\n                }\n            }\n\n            Column(\n                modifier = Modifier.fillMaxHeight(),\n                verticalArrangement = Arrangement.SpaceBetween\n            ) {\n                Text(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = spaceVideo.title,\n                    //style = MaterialTheme.typography.titleMedium,\n                    minLines = 2,\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis\n                )\n                Text(\n                    text = spaceVideo.publishDate.formatPubTimeString(context),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                )\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(2.dp)\n                    ) {\n                        Icon(\n                            painter = painterResource(id = R.drawable.ic_play_count),\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                        )\n                        Text(\n                            text = \"${spaceVideo.play}\",\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                        )\n                    }\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(2.dp)\n                    ) {\n                        Icon(\n                            painter = painterResource(id = R.drawable.ic_danmaku_count),\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                        )\n                        Text(\n                            text = \"${spaceVideo.danmaku}\",\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n                        )\n                    }\n\n                }\n            }\n        }\n    }\n\n}\n\n@Preview\n@Composable\nfun UpSpaceVideoItemPreview() {\n    BVMobileTheme {\n        Surface {\n            UpSpaceVideoItem(\n                spaceVideo = SpaceVideo(\n                    aid = 0,\n                    bvid = \"\",\n                    title = \"This is a video title! This is a video title! This is a video title!\",\n                    cover = \"\",\n                    author = \"\",\n                    duration = 2345,\n                    play = 4342,\n                    danmaku = 634,\n                    publishDate = Date()\n                )\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/DynamicDetailScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport android.content.Context\nimport androidx.activity.BackEventCompat\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.PredictiveBackHandler\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport coil.compose.rememberAsyncImagePainter\nimport coil.request.ImageRequest\nimport coil.size.Size\nimport com.origeek.imageViewer.previewer.ImagePreviewer\nimport com.origeek.imageViewer.previewer.ImagePreviewerState\nimport com.origeek.imageViewer.previewer.VerticalDragType\nimport com.origeek.imageViewer.previewer.rememberPreviewerState\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.biliapi.entity.reply.CommentSort\nimport dev.aaa1115910.biliapi.entity.user.DynamicItem\nimport dev.aaa1115910.bv.mobile.component.home.dynamic.DynamicContent\nimport dev.aaa1115910.bv.mobile.component.home.dynamic.DynamicHeader\nimport dev.aaa1115910.bv.mobile.component.reply.Comments\nimport dev.aaa1115910.bv.mobile.component.reply.Replies\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.swapList\nimport dev.aaa1115910.bv.viewmodel.CommentViewModel\nimport dev.aaa1115910.bv.viewmodel.DynamicDetailViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\nimport kotlin.math.min\n\n@OptIn(\n    ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class,\n    ExperimentalMaterial3WindowSizeClassApi::class\n)\n@Composable\nfun DynamicDetailScreen(\n    modifier: Modifier = Modifier,\n    dynamicDetailViewModel: DynamicDetailViewModel = koinViewModel(),\n    commentViewModel: CommentViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val windowSizeClass = calculateWindowSizeClass(context as Activity)\n    val pictures = remember { mutableStateListOf<Picture>() }\n    val dynamicDetailState = rememberDynamicDetailState(\n        dynamicDetailViewModel = dynamicDetailViewModel,\n        commentViewModel = commentViewModel,\n        imagePreviewerState = rememberPreviewerState(\n            verticalDragType = VerticalDragType.UpAndDown,\n            pageCount = { pictures.size },\n            getKey = { pictures[it].key }\n        )\n    )\n\n    val onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit =\n        { newPictures, afterSetPictures ->\n            pictures.swapList(newPictures)\n            afterSetPictures()\n        }\n\n    if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Expanded) {\n        DynamicDetailMobileContent(\n            modifier = modifier,\n            dynamicDetailState = dynamicDetailState,\n            onShowPreviewer = onShowPreviewer,\n        )\n    } else {\n        DynamicDetailScreenPadContent(\n            modifier = modifier,\n            dynamicDetailState = dynamicDetailState,\n            onShowPreviewer = onShowPreviewer,\n        )\n    }\n\n    ImagePreviewer(\n        modifier = Modifier\n            .fillMaxSize(),\n        state = dynamicDetailState.imagePreviewerState,\n        imageLoader = { index ->\n            val imageRequest = ImageRequest.Builder(context)\n                .data(pictures[index].url)\n                .size(Size.ORIGINAL)\n                .build()\n            rememberAsyncImagePainter(imageRequest)\n        }\n    )\n}\n\n@OptIn(\n    ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class,\n    ExperimentalMaterial3ExpressiveApi::class\n)\n@Composable\nfun DynamicDetailMobileContent(\n    modifier: Modifier = Modifier,\n    dynamicDetailState: DynamicDetailState,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit\n) {\n    val context = LocalContext.current\n    val density = LocalDensity.current\n    val logger = KotlinLogging.logger { }\n    val screenHeight = with(density) { context.resources.displayMetrics.heightPixels.toDp() }\n\n    var showMask by remember { mutableStateOf(false) }\n    var showReplies by remember { mutableStateOf(false) }\n\n    val onRepliesCloseAnimationFinish: (Dp) -> Unit = { finishDp ->\n        logger.fInfo { \"onRepliesCloseAnimationFinish: $finishDp\" }\n        if (finishDp == screenHeight) {\n            showReplies = false\n            showMask = false\n        }\n    }\n\n    var maskAlphaTarget by remember { mutableFloatStateOf(0.5f) }\n    val maskAlpha by animateFloatAsState(\n        targetValue = maskAlphaTarget,\n        animationSpec = spring(stiffness = Spring.StiffnessMediumLow),\n        label = \"replies scrim mask alpha\"\n    )\n    var repliesOffsetYTarget by remember { mutableStateOf(0.dp) }\n    val repliesOffsetY by animateDpAsState(\n        targetValue = repliesOffsetYTarget,\n        animationSpec = spring(stiffness = Spring.StiffnessMediumLow),\n        label = \"replies offset y\",\n        finishedListener = onRepliesCloseAnimationFinish\n    )\n    var repliesScaleTarget by remember { mutableFloatStateOf(1f) }\n    val repliesScale by animateFloatAsState(\n        targetValue = repliesScaleTarget,\n        label = \"replies scale\"\n    )\n    val repliesRoundCorner by animateDpAsState(\n        targetValue = if (repliesScaleTarget == 1f) 0.dp else 28.dp,\n        label = \"replies round corner\"\n    )\n\n    val onCloseReplies: () -> Unit = {\n        maskAlphaTarget = 0f\n        repliesOffsetYTarget = screenHeight\n    }\n\n    LaunchedEffect(showMask) {\n        maskAlphaTarget = if (showMask) 0.5f else 0f\n    }\n\n    LaunchedEffect(showReplies) {\n        repliesOffsetYTarget = if (showReplies) 0.dp else screenHeight\n        if (showReplies) repliesScaleTarget = 1f\n        showMask = showReplies\n    }\n\n    PredictiveBackHandler(showMask) { progress: Flow<BackEventCompat> ->\n        runCatching {\n            progress.collect { backEvent ->\n                maskAlphaTarget = (1 - backEvent.progress * 0.8f) * 0.5f\n                repliesOffsetYTarget = (backEvent.progress * 200).dp\n                repliesScaleTarget = 1 - min(0.6f, backEvent.progress) * 0.2f\n            }\n            onCloseReplies()\n        }.onFailure {\n            maskAlphaTarget = 0.5f\n            repliesOffsetYTarget = 0.dp\n        }\n    }\n\n    Box(\n        modifier = modifier.fillMaxSize()\n    ) {\n        Scaffold(\n            topBar = {\n                TopAppBar(\n                    title = { Text(\"Dynamic Detail\") },\n                    navigationIcon = {\n                        IconButton(onClick = dynamicDetailState.onExitActivity) {\n                            Icon(\n                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                                contentDescription = null\n                            )\n                        }\n                    }\n                )\n            }\n        ) { innerPadding ->\n            if (dynamicDetailState.dynamicItem != null) {\n                CommentPart(\n                    modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),\n                    previewerState = dynamicDetailState.imagePreviewerState,\n                    comments = dynamicDetailState.comments,\n                    commentSort = dynamicDetailState.commentSort,\n                    isLoading = dynamicDetailState.isLoadingComments,\n                    isRefreshing = dynamicDetailState.isRefreshingComments,\n                    onLoadMoreComments = dynamicDetailState::loadMoreComments,\n                    onRefreshComments = dynamicDetailState::refreshComments,\n                    onSwitchCommentSort = dynamicDetailState::switchCommentSort,\n                    onShowPreviewer = onShowPreviewer,\n                    onShowReplies = { comment ->\n                        dynamicDetailState.updateCurrentComment(comment)\n                        dynamicDetailState.refreshReplies()\n                        showReplies = true\n                    },\n                    header = {\n                        DynamicPart(\n                            modifier = Modifier,\n                            dynamicItem = dynamicDetailState.dynamicItem,\n                            previewerState = dynamicDetailState.imagePreviewerState,\n                            onShowPreviewer = onShowPreviewer\n                        )\n                    }\n                )\n            } else {\n                LoadingIndicator()\n            }\n        }\n\n        // Dark mask\n        if (showMask)\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .background(Color.Black.copy(alpha = maskAlpha))\n                    .clickable(\n                        interactionSource = null,\n                        indication = null,\n                        onClick = { }\n                    )\n            ) {}\n\n        // replies\n        if (showReplies) {\n            ReplyPart(\n                modifier = Modifier\n                    .offset(y = repliesOffsetY)\n                    .scale(repliesScale)\n                    .clip(\n                        RoundedCornerShape(\n                            topStart = repliesRoundCorner,\n                            topEnd = repliesRoundCorner\n                        )\n                    ),\n                comment = dynamicDetailState.replyComment,\n                sort = dynamicDetailState.replySort,\n                replies = dynamicDetailState.replies,\n                previewerState = dynamicDetailState.imagePreviewerState,\n                repliesCount = dynamicDetailState.replyComment?.repliesCount ?: 0,\n                isLoading = dynamicDetailState.isLoadingReplies,\n                isRefreshing = dynamicDetailState.isRefreshingReplies,\n                onShowPreviewer = onShowPreviewer,\n                onCloseReplies = onCloseReplies,\n                onSwitchSort = dynamicDetailState::switchReplySort,\n                onRefreshReplies = dynamicDetailState::refreshReplies,\n                onLoadMoreReplies = dynamicDetailState::loadMoreReplies,\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)\n@Composable\nfun DynamicDetailScreenPadContent(\n    modifier: Modifier = Modifier,\n    dynamicDetailState: DynamicDetailState,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit,\n) {\n    val context = LocalContext.current\n    val density = LocalDensity.current\n\n    val screenWidth = with(density) { context.resources.displayMetrics.widthPixels.toDp() }\n\n    var showReplies by remember { mutableStateOf(false) }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            TopAppBar(\n                title = { Text(\"Dynamic Detail\") },\n                navigationIcon = {\n                    IconButton(onClick = dynamicDetailState.onExitActivity) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                            contentDescription = null\n                        )\n                    }\n                }\n            )\n        }\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier.padding(top = innerPadding.calculateTopPadding())\n        ) {\n            Row(\n                modifier = Modifier.fillMaxSize(),\n                horizontalArrangement = Arrangement.Center\n            ) {\n                DynamicPart(\n                    modifier = Modifier\n                        .width(screenWidth / 3 - 10.dp)\n                        .verticalScroll(rememberScrollState()),\n                    dynamicItem = dynamicDetailState.dynamicItem,\n                    previewerState = dynamicDetailState.imagePreviewerState,\n                    onShowPreviewer = onShowPreviewer\n                )\n                AnimatedVisibility(\n                    visible = dynamicDetailState.dynamicItem != null,\n                    enter = expandHorizontally(),\n                    exit = shrinkHorizontally()\n                ) {\n                    CommentPart(\n                        modifier = Modifier.width(screenWidth / 3 - 10.dp),\n                        previewerState = dynamicDetailState.imagePreviewerState,\n                        comments = dynamicDetailState.comments,\n                        commentSort = dynamicDetailState.commentSort,\n                        isLoading = dynamicDetailState.isLoadingComments,\n                        isRefreshing = dynamicDetailState.isRefreshingComments,\n                        onLoadMoreComments = dynamicDetailState::loadMoreComments,\n                        onRefreshComments = dynamicDetailState::refreshComments,\n                        onSwitchCommentSort = dynamicDetailState::switchCommentSort,\n                        onShowPreviewer = onShowPreviewer,\n                        onShowReplies = { comment ->\n                            dynamicDetailState.updateCurrentComment(comment)\n                            dynamicDetailState.refreshReplies()\n                            showReplies = true\n                        }\n                    )\n                }\n                AnimatedVisibility(\n                    visible = showReplies,\n                    enter = expandHorizontally(),\n                    exit = shrinkHorizontally()\n                ) {\n                    ReplyPart(\n                        modifier = Modifier.width(screenWidth / 3 - 10.dp),\n                        comment = dynamicDetailState.replyComment,\n                        sort = dynamicDetailState.replySort,\n                        replies = dynamicDetailState.replies,\n                        previewerState = dynamicDetailState.imagePreviewerState,\n                        repliesCount = dynamicDetailState.replyComment?.repliesCount ?: 0,\n                        isLoading = dynamicDetailState.isLoadingReplies,\n                        isRefreshing = dynamicDetailState.isRefreshingReplies,\n                        enableTopPadding = false,\n                        onShowPreviewer = onShowPreviewer,\n                        onCloseReplies = { showReplies = false },\n                        onSwitchSort = dynamicDetailState::switchReplySort,\n                        onRefreshReplies = dynamicDetailState::refreshReplies,\n                        onLoadMoreReplies = dynamicDetailState::loadMoreReplies,\n                    )\n                }\n            }\n        }\n    }\n}\n\ndata class DynamicDetailState(\n    val context: Context,\n    val scope: CoroutineScope,\n    val dynamicDetailViewModel: DynamicDetailViewModel,\n    val commentViewModel: CommentViewModel,\n    val imagePreviewerState: ImagePreviewerState\n) {\n    val dynamicItem get() = dynamicDetailViewModel.dynamicItem\n    val comments get() = commentViewModel.comments\n    val replies get() = commentViewModel.replies\n    var replyComment by mutableStateOf<Comment?>(null)\n\n    val commentSort get() = commentViewModel.commentSort\n    val replySort get() = commentViewModel.replySort\n    val isRefreshingComments get() = commentViewModel.refreshingComments\n    val isRefreshingReplies get() = commentViewModel.refreshingReplies\n    val isLoadingComments get() = commentViewModel.updatingComments\n    val isLoadingReplies get() = commentViewModel.updatingReplies\n    val hasMoreComments get() = commentViewModel.hasMoreComments\n    val hasMoreReplies get() = commentViewModel.hasMoreReplies\n\n    fun loadMoreComments() {\n        scope.launch(Dispatchers.IO) {\n            commentViewModel.loadMoreComment()\n        }\n    }\n\n    fun loadMoreReplies() {\n        scope.launch(Dispatchers.IO) {\n            commentViewModel.loadMoreReplies()\n        }\n    }\n\n    fun updateCurrentComment(comment: Comment) {\n        replyComment = comment\n        commentViewModel.commentId = comment.oid\n        commentViewModel.commentType = comment.type\n        commentViewModel.rpid = comment.rpid\n    }\n\n    fun switchCommentSort(newSort: CommentSort) {\n        scope.launch(Dispatchers.IO) {\n            commentViewModel.switchCommentSort(newSort)\n        }\n    }\n\n    fun switchReplySort(newSort: CommentSort) {\n        scope.launch(Dispatchers.IO) {\n            commentViewModel.switchReplySort(newSort)\n        }\n    }\n\n    fun refreshComments() {\n        scope.launch(Dispatchers.IO) {\n            commentViewModel.refreshComments()\n        }\n    }\n\n    fun refreshReplies() {\n        scope.launch(Dispatchers.IO) {\n            commentViewModel.refreshReplies()\n        }\n    }\n\n    val onExitActivity: () -> Unit = { (context as Activity).finish() }\n}\n\n@Composable\nfun rememberDynamicDetailState(\n    dynamicDetailViewModel: DynamicDetailViewModel,\n    commentViewModel: CommentViewModel,\n    imagePreviewerState: ImagePreviewerState\n): DynamicDetailState {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n\n    BackHandler(imagePreviewerState.canClose || imagePreviewerState.animating) {\n        if (imagePreviewerState.canClose) scope.launch {\n            imagePreviewerState.closeTransform()\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        val intent = (context as Activity).intent\n        val dynamicId = intent.getStringExtra(\"dynamicId\")\n        dynamicId?.let { dynamicDetailViewModel.dynamicId = dynamicId } ?: context.finish()\n\n        scope.launch(Dispatchers.IO) {\n            dynamicDetailViewModel.loadDynamic()\n            if (dynamicDetailViewModel.dynamicItem?.commentId != null && dynamicDetailViewModel.dynamicItem?.commentType != null) {\n                commentViewModel.commentId = dynamicDetailViewModel.dynamicItem!!.commentId\n                commentViewModel.commentType = dynamicDetailViewModel.dynamicItem!!.commentType\n                //commentViewModel.loadMoreComment()\n            }\n        }\n    }\n\n    return remember(\n        dynamicDetailViewModel,\n        commentViewModel,\n        imagePreviewerState\n    ) {\n        DynamicDetailState(\n            context = context,\n            scope = scope,\n            dynamicDetailViewModel = dynamicDetailViewModel,\n            commentViewModel = commentViewModel,\n            imagePreviewerState = imagePreviewerState\n        )\n    }\n}\n\n@Composable\nprivate fun DynamicPart(\n    modifier: Modifier = Modifier,\n    dynamicItem: DynamicItem?,\n    previewerState: ImagePreviewerState,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit\n) {\n    Column(\n        modifier = modifier\n    ) {\n        if (dynamicItem != null) {\n            DynamicHeader(\n                modifier = Modifier\n                    .padding(12.dp),\n                author = dynamicItem.author\n            )\n        }\n        if (dynamicItem != null) {\n            DynamicContent(\n                modifier = Modifier\n                    .padding(\n                        bottom = WindowInsets.navigationBars.asPaddingValues()\n                            .calculateBottomPadding()\n                    ),\n                dynamicItem = dynamicItem,\n                previewerState = previewerState,\n                onShowPreviewer = onShowPreviewer,\n                onClick = { }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun CommentPart(\n    modifier: Modifier = Modifier,\n    header: (@Composable () -> Unit)? = null,\n    previewerState: ImagePreviewerState,\n    comments: List<Comment>,\n    commentSort: CommentSort,\n    isLoading: Boolean,\n    isRefreshing: Boolean,\n    onLoadMoreComments: () -> Unit,\n    onRefreshComments: () -> Unit,\n    onSwitchCommentSort: (CommentSort) -> Unit,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit,\n    onShowReplies: (comment: Comment) -> Unit\n) {\n    Comments(\n        modifier = modifier,\n        header = header,\n        previewerState = previewerState,\n        comments = comments,\n        commentSort = commentSort,\n        isLoading = isLoading,\n        isRefreshing = isRefreshing,\n        onLoadMoreComments = onLoadMoreComments,\n        onRefreshComments = onRefreshComments,\n        onSwitchCommentSort = onSwitchCommentSort,\n        onShowPreviewer = onShowPreviewer,\n        onShowReplies = onShowReplies,\n    )\n}\n\n@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)\n@Composable\nprivate fun ReplyPart(\n    modifier: Modifier = Modifier,\n    previewerState: ImagePreviewerState,\n    comment: Comment?,\n    sort: CommentSort,\n    replies: List<Comment>,\n    repliesCount: Int,\n    isLoading: Boolean,\n    isRefreshing: Boolean,\n    enableTopPadding: Boolean = true,\n    onSwitchSort: (CommentSort) -> Unit,\n    onShowPreviewer: (List<Picture>, () -> Unit) -> Unit,\n    onCloseReplies: () -> Unit,\n    onRefreshReplies: () -> Unit,\n    onLoadMoreReplies: () -> Unit\n) {\n    Scaffold(\n        modifier = modifier\n            .fillMaxSize(),\n        topBar = {\n            TopAppBar(\n                modifier = Modifier,\n                title = { Text(\"Replies\") },\n                navigationIcon = {\n                    IconButton(onClick = onCloseReplies) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                            contentDescription = null\n                        )\n                    }\n                },\n                windowInsets = if (enableTopPadding) TopAppBarDefaults.windowInsets\n                else WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)\n            )\n        }\n    ) { innerPadding ->\n        Replies(\n            modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),\n            previewerState = previewerState,\n            rootComment = comment,\n            replySort = sort,\n            replies = replies,\n            repliesCount = repliesCount,\n            isLoading = isLoading,\n            isRefreshing = isRefreshing,\n            onSwitchReplySort = onSwitchSort,\n            onShowPreviewer = onShowPreviewer,\n            onLoadMoreReplies = onLoadMoreReplies,\n            onRefreshReplies = onRefreshReplies,\n        )\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/FavoriteScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.PrimaryScrollableTabRow\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Tab\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.FavoriteFolderMetadata\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.mobile.activities.VideoPlayerActivity\nimport dev.aaa1115910.bv.mobile.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.OnBottomReached\nimport dev.aaa1115910.bv.util.calculateWindowSizeClassInPreview\nimport dev.aaa1115910.bv.viewmodel.user.FavoriteViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun FavoriteScreen(\n    modifier: Modifier = Modifier,\n    windowSize: WindowSizeClass,\n    favoriteViewModel: FavoriteViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val listState = rememberLazyGridState()\n\n    val currentTabIndex by remember {\n        derivedStateOf {\n            favoriteViewModel.favoriteFolderMetadataList.indexOf(favoriteViewModel.currentFavoriteFolderMetadata)\n        }\n    }\n\n    if (favoriteViewModel.favoriteFolderMetadataList.isNotEmpty() && favoriteViewModel.favorites.isNotEmpty()) {\n        listState.OnBottomReached(\n            loading = favoriteViewModel.updatingFolderItems,\n        ) {\n            favoriteViewModel.updateFolderItems()\n        }\n    }\n\n    LaunchedEffect(currentTabIndex) {\n        favoriteViewModel.favorites.clear()\n        favoriteViewModel.updateFolderItems(force = true)\n    }\n\n    FavoriteContent(\n        modifier = modifier,\n        listState = listState,\n        windowSize = windowSize,\n        selectedTabIndex = currentTabIndex,\n        favoriteFolders = favoriteViewModel.favoriteFolderMetadataList,\n        favorites = favoriteViewModel.favorites,\n        onClickTab = { folderMetadata ->\n            favoriteViewModel.currentFavoriteFolderMetadata = folderMetadata\n        },\n        onClickVideo = { videoCardData ->\n            VideoPlayerActivity.actionStart(\n                context = context,\n                aid = videoCardData.avid,\n            )\n        },\n        onBack = { (context as Activity).finish() }\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun FavoriteContent(\n    modifier: Modifier = Modifier,\n    listState: LazyGridState = rememberLazyGridState(),\n    windowSize: WindowSizeClass,\n    selectedTabIndex: Int,\n    favoriteFolders: List<FavoriteFolderMetadata>,\n    favorites: List<VideoCardData>,\n    onClickTab: (FavoriteFolderMetadata) -> Unit,\n    onClickVideo: (VideoCardData) -> Unit,\n    onBack: () -> Unit\n) {\n    val scrollBehavior =\n        TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    Scaffold(\n        modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            Column {\n                LargeTopAppBar(\n                    title = { Text(text = stringResource(id = R.string.title_mobile_activity_favorite)) },\n                    navigationIcon = {\n                        IconButton(onClick = onBack) {\n                            Icon(\n                                imageVector = Icons.AutoMirrored.Default.ArrowBack,\n                                contentDescription = null\n                            )\n                        }\n                    },\n                    scrollBehavior = scrollBehavior\n                )\n\n                if (favoriteFolders.isNotEmpty()) {\n                    PrimaryScrollableTabRow(\n                        selectedTabIndex = selectedTabIndex,\n                        divider = { },\n                    ) {\n                        favoriteFolders.forEachIndexed { index, folderMetadata ->\n                            Tab(\n                                selected = selectedTabIndex == index,\n                                onClick = { onClickTab(folderMetadata) }\n                            ) {\n                                Box(\n                                    modifier = Modifier.height(48.dp),\n                                    contentAlignment = Alignment.Center\n                                ) {\n                                    Text(\n                                        modifier = Modifier.padding(horizontal = 16.dp),\n                                        text = folderMetadata.title,\n                                        style = MaterialTheme.typography.bodyLarge,\n                                        textAlign = TextAlign.Center\n                                    )\n                                }\n\n                            }\n                        }\n                    }\n                    HorizontalDivider()\n                }\n            }\n        }\n    ) { innerPadding ->\n        LazyVerticalGrid(\n            modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),\n            state = listState,\n            columns = GridCells.Adaptive(180.dp),\n            contentPadding = PaddingValues(12.dp),\n            verticalArrangement = Arrangement.spacedBy(12.dp),\n            horizontalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            itemsIndexed(items = favorites) { index, data ->\n                SmallVideoCard(\n                    data = data,\n                    onClick = { onClickVideo(data) }\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Preview(device = \"spec:width=411dp,height=891dp\")\n@Preview(device = \"spec:width=1280dp,height=800dp,dpi=240\")\n@Composable\nprivate fun FavoriteContentPreview() {\n    val windowSize = calculateWindowSizeClassInPreview()\n    val favoriteFolderSize = 10\n    var currentFavoriteFolderMetadata by remember { mutableStateOf<FavoriteFolderMetadata?>(null) }\n\n    val favoriteFolderMetadataList = (1..favoriteFolderSize).map {\n        FavoriteFolderMetadata(\n            id = it.toLong(),\n            fid = it.toLong(),\n            mid = 0,\n            title = \"folder$it\",\n            cover = null,\n            videoInThisFav = false,\n            mediaCount = (30..50).random()\n        )\n    }\n\n    val generateFavorites: (Long) -> List<VideoCardData> = { folderId ->\n        (1..(currentFavoriteFolderMetadata?.mediaCount ?: 50)).map {\n            VideoCardData(\n                avid = it.toLong(),\n                title = \"folder$folderId video$it\",\n                cover = \"\",\n                play = it * 1000L,\n                danmaku = it * 100,\n                upName = \"upName$it\",\n                time = it * 1000L\n            )\n        }\n    }\n\n    val currentTabIndex by remember {\n        derivedStateOf {\n            favoriteFolderMetadataList.indexOf(currentFavoriteFolderMetadata)\n                .takeIf { it != -1 } ?: 0\n        }\n    }\n    val favorites by remember {\n        derivedStateOf { generateFavorites(currentFavoriteFolderMetadata?.id ?: 0) }\n    }\n\n    BVMobileTheme {\n        FavoriteContent(\n            windowSize = windowSize,\n            selectedTabIndex = currentTabIndex,\n            favoriteFolders = favoriteFolderMetadataList,\n            favorites = favorites,\n            onClickTab = { currentFavoriteFolderMetadata = it },\n            onClickVideo = {},\n            onBack = {}\n        )\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/FollowingSeasonScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeTopAppBar\nimport androidx.compose.material3.PrimaryTabRow\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Tab\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonType\nimport dev.aaa1115910.bv.entity.carddata.SeasonCardData\nimport dev.aaa1115910.bv.mobile.component.videocard.SeasonCard\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.OnBottomReached\nimport dev.aaa1115910.bv.util.calculateWindowSizeClassInPreview\nimport dev.aaa1115910.bv.util.getDisplayName\nimport dev.aaa1115910.bv.viewmodel.user.FollowingSeasonViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun FollowingSeasonScreen(\n    modifier: Modifier = Modifier,\n    windowSize: WindowSizeClass,\n    followingSeasonViewModel: FollowingSeasonViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val listState = rememberLazyGridState()\n\n    listState.OnBottomReached(\n        loading = followingSeasonViewModel.updating\n    ) {\n        if (followingSeasonViewModel.noMore) return@OnBottomReached\n        followingSeasonViewModel.loadMore()\n    }\n\n    FollowingSeasonContent(\n        modifier = modifier,\n        windowSize = windowSize,\n        type = followingSeasonViewModel.followingSeasonType,\n        seasons = followingSeasonViewModel.followingSeasons.map(SeasonCardData::fromFollowingSeason),\n        onBack = { (context as Activity).finish() },\n        onTypeChange = {\n            followingSeasonViewModel.followingSeasonType = it\n            followingSeasonViewModel.clearData()\n            followingSeasonViewModel.loadMore()\n        },\n        onClickSeason = {}\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun FollowingSeasonContent(\n    modifier: Modifier = Modifier,\n    windowSize: WindowSizeClass,\n    type: FollowingSeasonType,\n    seasons: List<SeasonCardData>,\n    onBack: () -> Unit,\n    onTypeChange: (FollowingSeasonType) -> Unit,\n    onClickSeason: (SeasonCardData) -> Unit,\n) {\n    val context = LocalContext.current\n    val scrollBehavior =\n        TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    Scaffold(\n        modifier = modifier\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            Column {\n                LargeTopAppBar(\n                    title = { Text(text = \"我的追番\") },\n                    navigationIcon = {\n                        IconButton(onClick = onBack) {\n                            Icon(\n                                imageVector = Icons.AutoMirrored.Default.ArrowBack,\n                                contentDescription = null\n                            )\n                        }\n                    },\n                    scrollBehavior = scrollBehavior\n                )\n                PrimaryTabRow(\n                    selectedTabIndex = type.ordinal,\n                ) {\n                    FollowingSeasonType.entries.forEach { seasonType ->\n                        Tab(\n                            selected = type == seasonType,\n                            text = { Text(text = seasonType.getDisplayName(context)) },\n                            onClick = { onTypeChange(seasonType) }\n                        )\n                    }\n                }\n            }\n        },\n    ) { innerPadding ->\n        LazyVerticalGrid(\n            modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),\n            columns = GridCells.Adaptive(100.dp),\n            contentPadding = PaddingValues(12.dp),\n            verticalArrangement = Arrangement.spacedBy(12.dp),\n            horizontalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            itemsIndexed(seasons) { index, season ->\n                SeasonCard(\n                    data = season,\n                    onClick = { onClickSeason(season) }\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Preview\n@Composable\nprivate fun FollowingSeasonContentPreview() {\n    val windowSize = calculateWindowSizeClassInPreview()\n    var selectedType by remember { mutableStateOf(FollowingSeasonType.Bangumi) }\n\n    val seasons = (1..50).map {\n        SeasonCardData(\n            seasonId = it,\n            title = \"Title $it\",\n            cover = \"http://i0.hdslb.com/bfs/bangumi/image/8d211c396aad084d6fa413015200dda6ed260768.png\",\n            rating = \"8.6\"\n        )\n    }\n\n    BVMobileTheme {\n        FollowingSeasonContent(\n            windowSize = windowSize,\n            type = selectedType,\n            seasons = seasons,\n            onBack = {},\n            onTypeChange = { selectedType = it },\n            onClickSeason = {}\n        )\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/FollowingUserScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeTopAppBar\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.user.FollowedUser\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.mobile.activities.UserSpaceActivity\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.viewmodel.user.FollowViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3WindowSizeClassApi::class)\n@Composable\nfun FollowingUserScreen(\n    modifier: Modifier = Modifier,\n    followViewModel: FollowViewModel = koinViewModel(),\n) {\n    val context = LocalContext.current\n    val windowSizeClass = calculateWindowSizeClass(context as Activity)\n\n    val scrollBehavior =\n        TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    val onClickUser: (FollowedUser) -> Unit = { followedUser ->\n        UserSpaceActivity.actionStart(\n            context = context,\n            mid = followedUser.mid,\n            name = followedUser.name\n        )\n    }\n\n    Scaffold(\n        modifier = modifier\n            .fillMaxSize()\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            LargeTopAppBar(\n                title = { Text(text = stringResource(R.string.title_mobile_activity_following_user)) },\n                navigationIcon = {\n                    IconButton(onClick = { context.finish() }) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Default.ArrowBack,\n                            contentDescription = null\n                        )\n                    }\n                },\n                scrollBehavior = scrollBehavior\n            )\n        }\n    ) { innerPadding ->\n        if (followViewModel.updating) {\n            Box(\n                modifier = Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center\n            ) {\n                Loading()\n            }\n        }\n\n        if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Expanded) {\n            LazyColumn(\n                modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),\n            ) {\n                if (!followViewModel.updating) {\n                    items(items = followViewModel.followedUsers) { followedUser ->\n                        FollowingUserListItem(\n                            followedUser = followedUser,\n                            onClick = onClickUser\n                        )\n                    }\n                    item {\n                        Spacer(modifier = Modifier.navigationBarsPadding())\n                    }\n                }\n            }\n        } else {\n            LazyVerticalGrid(\n                modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),\n                columns = GridCells.Fixed(2)\n            ) {\n                if (!followViewModel.updating) {\n                    items(items = followViewModel.followedUsers) { followedUser ->\n                        FollowingUserListItem(\n                            followedUser = followedUser,\n                            onClick = onClickUser\n                        )\n                    }\n                    item {\n                        Spacer(modifier = Modifier.navigationBarsPadding())\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun FollowingUserListItem(\n    modifier: Modifier = Modifier,\n    followedUser: FollowedUser,\n    onClick: (FollowedUser) -> Unit = {}\n) {\n    ListItem(\n        modifier = modifier\n            .clickable { onClick(followedUser) },\n        headlineContent = { Text(text = followedUser.name) },\n        supportingContent = {\n            Text(\n                text = followedUser.sign,\n                maxLines = 2,\n                overflow = TextOverflow.Ellipsis\n            )\n        },\n        leadingContent = {\n            AsyncImage(\n                modifier = Modifier\n                    .size(48.dp)\n                    .background(Color.Gray, CircleShape)\n                    .clip(CircleShape),\n                model = followedUser.avatar,\n                contentDescription = null,\n                contentScale = ContentScale.FillBounds\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun Loading(\n    modifier: Modifier = Modifier\n) {\n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(24.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(24.dp)\n    ) {\n        Text(text = \"Loading\")\n        LinearProgressIndicator(\n            modifier = Modifier.fillMaxWidth()\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun FollowingUserListItemPreview() {\n    BVMobileTheme {\n        FollowingUserListItem(\n            followedUser = FollowedUser(\n                mid = 1L,\n                name = \"UP name\",\n                avatar = \"https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg@450w_450h_progressive.webp\",\n                sign = \"This is a sign\"\n            )\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun LoadingPreview() {\n    BVMobileTheme {\n        Surface {\n            Loading()\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/HistoryScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeTopAppBar\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.material3.windowsizeclass.WindowSizeClass\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.mobile.activities.VideoPlayerActivity\nimport dev.aaa1115910.bv.mobile.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.util.OnBottomReached\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.viewmodel.user.HistoryViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport org.koin.androidx.compose.koinViewModel\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun HistoryScreen(\n    modifier: Modifier = Modifier,\n    windowSize: WindowSizeClass,\n    historyViewModel: HistoryViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val logger = KotlinLogging.logger(\"HistoryScreen\")\n    val listState = rememberLazyGridState()\n    val scrollBehavior =\n        TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    listState.OnBottomReached(\n        loading = historyViewModel.updating\n    ) {\n        logger.fInfo { \"on reached rcmd page bottom\" }\n        historyViewModel.update()\n    }\n\n    Scaffold(\n        modifier = modifier\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            LargeTopAppBar(\n                title = { Text(text = stringResource(R.string.title_mobile_activity_history)) },\n                navigationIcon = {\n                    IconButton(\n                        onClick = { (context as Activity).finish() }\n                    ) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Default.ArrowBack,\n                            contentDescription = null\n                        )\n                    }\n                },\n                scrollBehavior = scrollBehavior\n            )\n        }\n    ) { innerPadding ->\n        LazyVerticalGrid(\n            modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),\n            columns = GridCells.Adaptive(if (windowSize.widthSizeClass == WindowWidthSizeClass.Compact) 180.dp else 220.dp),\n            state = listState,\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n            contentPadding = PaddingValues(8.dp)\n        ) {\n            items(historyViewModel.histories) { history ->\n                SmallVideoCard(\n                    data = history,\n                    onClick = {\n                        VideoPlayerActivity.actionStart(\n                            context = context,\n                            aid = history.avid\n                        )\n                    }\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/LoginScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.VerticalDivider\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowSizeClass\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport com.geetest.sdk.GT3ConfigBean\nimport com.geetest.sdk.GT3ErrorBean\nimport com.geetest.sdk.GT3GeetestUtils\nimport com.geetest.sdk.GT3Listener\nimport dev.aaa1115910.biliapi.entity.login.QrLoginState\nimport dev.aaa1115910.biliapi.repositories.SendSmsState\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.component.QrImage\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.calculateWindowSizeClassInPreview\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.login.AppQrLoginViewModel\nimport dev.aaa1115910.bv.viewmodel.login.GeetestResult\nimport dev.aaa1115910.bv.viewmodel.login.SmsLoginViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.json.Json\nimport org.json.JSONObject\nimport org.koin.androidx.compose.koinViewModel\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Composable\nfun LoginScreen(\n    modifier: Modifier = Modifier,\n    smsLoginViewModel: SmsLoginViewModel = koinViewModel(),\n    appQrLoginViewModel: AppQrLoginViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val logger = KotlinLogging.logger { }\n    val scope = rememberCoroutineScope()\n    val keyboardController = LocalSoftwareKeyboardController.current\n    val windowSize = calculateWindowSizeClass(context as Activity)\n\n    var gt3GeetestUtils: GT3GeetestUtils? by remember { mutableStateOf(null) }\n    val gt3ConfigBean by remember { mutableStateOf(GT3ConfigBean()) }\n    var phone by remember { mutableLongStateOf(0) }\n\n    val setConfig: (challenge: String, gt: String) -> Unit = { challenge, gt ->\n        gt3GeetestUtils!!.startCustomFlow()\n        gt3ConfigBean.api1Json = JSONObject().apply {\n            put(\"success\", 1)\n            put(\"gt\", gt)\n            put(\"challenge\", challenge)\n        }\n        gt3GeetestUtils!!.getGeetest()\n    }\n\n    val sendSms: (Long) -> Unit = { phoneNumber ->\n        phone = phoneNumber\n        keyboardController?.hide()\n        scope.launch(Dispatchers.IO) {\n            runCatching {\n                smsLoginViewModel.sendSms(phoneNumber) { challenge: String, gt: String ->\n                    scope.launch(Dispatchers.Main) {\n                        setConfig(challenge, gt)\n                    }\n                }\n            }\n        }\n    }\n\n    val loginWithSms: (Long, Int) -> Unit = { phoneNumber, code ->\n        phone = phoneNumber\n        keyboardController?.hide()\n        if (smsLoginViewModel.sendSmsState != SendSmsState.Success) {\n            R.string.sms_login_toast_send_sms_first.toast(context)\n        } else {\n            scope.launch(Dispatchers.IO) {\n                runCatching {\n                    smsLoginViewModel.loginWithSms(code) {\n                        scope.launch(Dispatchers.Main) {\n                            R.string.login_success.toast(context)\n                        }\n                        (context as Activity).finish()\n                    }\n                }\n            }\n        }\n    }\n\n    DisposableEffect(Unit) {\n        gt3GeetestUtils = GT3GeetestUtils(context)\n        gt3ConfigBean.apply {\n            pattern = 1\n            isCanceledOnTouchOutside = false\n            lang = null\n            timeout = 10000\n            webviewTimeout = 10000\n            corners = 24\n            listener = object : GT3Listener() {\n                override fun onReceiveCaptchaCode(p0: Int) {\n                    logger.info { \"Geetest - onReceiveCaptchaCode: $p0\" }\n                }\n\n                override fun onStatistics(p0: String?) {\n                    logger.info { \"Geetest - onStatistics: $p0\" }\n                }\n\n                override fun onClosed(p0: Int) {\n                    logger.info { \"Geetest - onClosed: $p0\" }\n                    smsLoginViewModel.clearCaptchaData()\n                }\n\n                override fun onSuccess(p0: String?) {\n                    logger.info { \"Geetest - onSuccess: $p0\" }\n                }\n\n                override fun onFailed(p0: GT3ErrorBean?) {\n                    logger.info { \"Geetest - onFailed: $p0\" }\n                    smsLoginViewModel.clearCaptchaData()\n                }\n\n                override fun onButtonClick() {\n                    logger.info { \"Geetest - onButtonClick\" }\n                }\n\n                override fun onDialogResult(result: String) {\n                    logger.info { \"Geetest - onDialogResult: $result\" }\n                    runCatching {\n                        val geetestResult = Json.decodeFromString<GeetestResult>(result)\n                        smsLoginViewModel.geetestChallenge = geetestResult.geetestChallenge\n                        smsLoginViewModel.geetestValidate = geetestResult.geetestValidate\n                        smsLoginViewModel.sendSmsState = SendSmsState.Ready\n                        gt3GeetestUtils?.showSuccessDialog()\n                        scope.launch(Dispatchers.IO) {\n                            smsLoginViewModel.sendSms(phone) { _, _ -> }\n                        }\n                    }.onFailure {\n                        gt3GeetestUtils?.showFailedDialog()\n                    }\n                }\n            }\n        }\n        gt3GeetestUtils!!.init(gt3ConfigBean)\n\n        onDispose {\n            gt3GeetestUtils?.destory()\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        appQrLoginViewModel.requestQRCode()\n    }\n\n    LaunchedEffect(appQrLoginViewModel.state) {\n        when (appQrLoginViewModel.state) {\n            QrLoginState.Success -> {\n                R.string.login_success.toast(context)\n                context.finish()\n            }\n\n            QrLoginState.Expired -> {\n                appQrLoginViewModel.requestQRCode()\n            }\n\n            else -> {}\n        }\n    }\n\n    DisposableEffect(Unit) {\n        onDispose {\n            appQrLoginViewModel.cancelCheckLoginResultTimer()\n        }\n    }\n\n    LoginContent(\n        modifier = modifier,\n        windowSize = windowSize,\n        qrLoginUrl = appQrLoginViewModel.loginUrl,\n        onBack = { context.finish() },\n        onClearCaptchaData = { smsLoginViewModel.clearCaptchaData() },\n        onSendSms = sendSms,\n        onLogin = loginWithSms\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun LoginContent(\n    modifier: Modifier = Modifier,\n    windowSize: WindowSizeClass,\n    qrLoginUrl: String,\n    onBack: () -> Unit,\n    onClearCaptchaData: () -> Unit,\n    onSendSms: (Long) -> Unit,\n    onLogin: (phoneNumber: Long, code: Int) -> Unit\n) {\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            LargeTopAppBar(\n                title = {\n                    Text(text = stringResource(id = R.string.title_mobile_activity_login))\n                },\n                navigationIcon = {\n                    IconButton(onClick = onBack) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                            contentDescription = \"Back\"\n                        )\n                    }\n                }\n            )\n        }\n    ) { innerPadding ->\n        when (windowSize.widthSizeClass) {\n            WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> LoginContentCompact(\n                modifier = Modifier.padding(innerPadding),\n                qrLoginUrl = qrLoginUrl,\n                onClearCaptchaData = onClearCaptchaData,\n                onSendSms = onSendSms,\n                onLogin = onLogin\n            )\n\n            WindowWidthSizeClass.Expanded -> LoginContentExpanded(\n                modifier = Modifier.padding(innerPadding),\n                qrLoginUrl = qrLoginUrl,\n                onClearCaptchaData = onClearCaptchaData,\n                onSendSms = onSendSms,\n                onLogin = onLogin\n            )\n        }\n    }\n}\n\n@Composable\nfun LoginContentCompact(\n    modifier: Modifier = Modifier,\n    qrLoginUrl: String,\n    onClearCaptchaData: () -> Unit,\n    onSendSms: (Long) -> Unit,\n    onLogin: (phoneNumber: Long, code: Int) -> Unit\n) {\n    var showQrCode by remember { mutableStateOf(false) }\n\n    Box(\n        modifier = modifier\n            .fillMaxSize(),\n        contentAlignment = Alignment.TopCenter\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            SmsLoginInputs(\n                modifier = Modifier\n                    .padding(24.dp)\n                    .widthIn(max = 400.dp),\n                onClearCaptchaData = onClearCaptchaData,\n                onSendSms = onSendSms,\n                onLogin = onLogin\n            )\n            AnimatedVisibility(showQrCode) {\n                QrImage(\n                    modifier = Modifier\n                        .padding(top = 36.dp)\n                        .size(240.dp),\n                    content = qrLoginUrl\n                )\n            }\n        }\n        if (!showQrCode) {\n            TextButton(\n                modifier = Modifier.align(Alignment.BottomCenter),\n                onClick = { showQrCode = true }\n            ) {\n                Text(text = stringResource(dev.aaa1115910.bv.mobile.R.string.qr_login_button_login))\n            }\n        }\n    }\n}\n\n@Composable\nfun LoginContentExpanded(\n    modifier: Modifier = Modifier,\n    qrLoginUrl: String,\n    onClearCaptchaData: () -> Unit,\n    onSendSms: (Long) -> Unit,\n    onLogin: (phoneNumber: Long, code: Int) -> Unit\n) {\n    Row(\n        modifier = modifier\n    ) {\n        Box(\n            modifier = Modifier\n                .weight(1f)\n                .fillMaxHeight(),\n            contentAlignment = Alignment.Center\n        ) {\n            SmsLoginInputs(\n                modifier = Modifier\n                    .width(400.dp),\n                onClearCaptchaData = onClearCaptchaData,\n                onSendSms = onSendSms,\n                onLogin = onLogin\n            )\n        }\n        VerticalDivider(\n            modifier = Modifier\n                .fillMaxHeight(0.5f)\n                .align(Alignment.CenterVertically)\n        )\n        Box(\n            modifier = Modifier\n                .weight(1f)\n                .fillMaxHeight(),\n            contentAlignment = Alignment.Center\n        ) {\n            QrLogin(\n                modifier = Modifier,\n                qrLoginUrl = qrLoginUrl\n            )\n\n        }\n    }\n}\n\n@Composable\nfun SmsLoginInputs(\n    modifier: Modifier = Modifier,\n    onClearCaptchaData: () -> Unit,\n    onSendSms: (Long) -> Unit,\n    onLogin: (phoneNumber: Long, code: Int) -> Unit\n) {\n    val keyboardController = LocalSoftwareKeyboardController.current\n\n    var phoneNumberText by remember { mutableStateOf(\"\") }\n    val phoneNumber by remember(phoneNumberText) {\n        mutableLongStateOf(runCatching { phoneNumberText.toLong() }.getOrNull() ?: 0)\n    }\n    var codeText by remember { mutableStateOf(\"\") }\n    val code by remember(codeText) {\n        mutableIntStateOf(runCatching { codeText.toInt() }.getOrNull() ?: 0)\n    }\n\n    val sendSmsButtonEnabled by remember(phoneNumber) {\n        derivedStateOf { phoneNumber != 0L && phoneNumberText.length == 11 }\n    }\n    val loginButtonEnabled by remember(code) {\n        derivedStateOf { sendSmsButtonEnabled && code != 0 && codeText.length == 6 }\n    }\n\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.Start,\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        OutlinedTextField(\n            modifier = Modifier.fillMaxWidth(),\n            value = phoneNumberText,\n            onValueChange = {\n                phoneNumberText = it\n                onClearCaptchaData()\n            },\n            label = { Text(text = stringResource(R.string.sms_login_phone_number)) },\n            maxLines = 1,\n            shape = MaterialTheme.shapes.medium,\n            keyboardOptions = KeyboardOptions(\n                keyboardType = KeyboardType.Phone,\n                imeAction = ImeAction.Send\n            ),\n            keyboardActions = KeyboardActions(\n                onSend = {\n                    if (sendSmsButtonEnabled) {\n                        onSendSms(phoneNumber)\n                        keyboardController?.hide()\n                    }\n                }\n            ),\n            trailingIcon = {\n                TextButton(\n                    onClick = {\n                        onSendSms(phoneNumber)\n                        keyboardController?.hide()\n                    },\n                    enabled = sendSmsButtonEnabled\n                ) {\n                    Text(text = stringResource(R.string.sms_login_button_send_sms))\n                }\n            }\n        )\n\n        OutlinedTextField(\n            modifier = Modifier.fillMaxWidth(),\n            value = codeText,\n            onValueChange = { codeText = it },\n            label = { Text(text = stringResource(R.string.sms_login_code)) },\n            maxLines = 1,\n            shape = MaterialTheme.shapes.medium,\n            keyboardOptions = KeyboardOptions(\n                keyboardType = KeyboardType.Number,\n                imeAction = ImeAction.Done\n            ),\n            keyboardActions = KeyboardActions(\n                onDone = {\n                    if (loginButtonEnabled) {\n                        onLogin(phoneNumber, code)\n                        keyboardController?.hide()\n                    }\n                }\n            )\n        )\n\n        Button(\n            modifier = Modifier.fillMaxWidth(),\n            onClick = {\n                onLogin(phoneNumber, code)\n                keyboardController?.hide()\n            },\n            enabled = loginButtonEnabled\n        ) {\n            Text(text = stringResource(R.string.sms_login_button_login))\n        }\n    }\n}\n\n@Composable\nfun QrLogin(\n    modifier: Modifier = Modifier,\n    qrLoginUrl: String\n) {\n    Box(\n        modifier = modifier\n    ) {\n        QrImage(\n            modifier = Modifier.size(240.dp),\n            content = qrLoginUrl\n        )\n    }\n}\n\n@Preview\n@Composable\nfun SmsLoginInputsPreview() {\n    BVMobileTheme {\n        Surface {\n            SmsLoginInputs(\n                modifier = Modifier.padding(24.dp),\n                onClearCaptchaData = {},\n                onSendSms = {},\n                onLogin = { _, _ -> }\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Preview\n@Preview(device = \"spec:width=1280dp,height=800dp,dpi=240\")\n@Preview(device = \"spec:width=1280dp,height=800dp,dpi=240,orientation=portrait\")\n@Composable\nprivate fun LoginScreenPreview() {\n    val windowSize = calculateWindowSizeClassInPreview()\n\n    BVMobileTheme {\n        LoginContent(\n            modifier = Modifier,\n            windowSize = windowSize,\n            qrLoginUrl = \"https://www.example.com\",\n            onBack = {},\n            onClearCaptchaData = {},\n            onSendSms = { _ -> },\n            onLogin = { _, _ -> }\n        )\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/MobileMainScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedContentTransitionScope\nimport androidx.compose.animation.EnterTransition\nimport androidx.compose.animation.ExitTransition\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState\nimport androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.rounded.Segment\nimport androidx.compose.material.icons.rounded.FiberNew\nimport androidx.compose.material.icons.rounded.Home\nimport androidx.compose.material.icons.rounded.Person\nimport androidx.compose.material.icons.rounded.Search\nimport androidx.compose.material.icons.rounded.Settings\nimport androidx.compose.material3.DrawerState\nimport androidx.compose.material3.DrawerValue\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.NavigationRail\nimport androidx.compose.material3.NavigationRailItem\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.adaptive.currentWindowAdaptiveInfo\nimport androidx.compose.material3.adaptive.navigationsuite.NavigationSuite\nimport androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults\nimport androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldLayout\nimport androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType\nimport androidx.compose.material3.rememberDrawerState\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowSizeClass\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport androidx.navigation.NavBackStackEntry\nimport androidx.navigation.NavGraph\nimport androidx.navigation.NavGraph.Companion.findStartDestination\nimport androidx.navigation.NavHostController\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport androidx.navigation.compose.rememberNavController\nimport coil.compose.AsyncImage\nimport coil.compose.rememberAsyncImagePainter\nimport coil.request.ImageRequest\nimport coil.size.Size\nimport com.origeek.imageViewer.previewer.ImagePreviewer\nimport com.origeek.imageViewer.previewer.VerticalDragType\nimport com.origeek.imageViewer.previewer.rememberPreviewerState\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.bv.component.DevelopingTipContent\nimport dev.aaa1115910.bv.mobile.activities.FavoriteActivity\nimport dev.aaa1115910.bv.mobile.activities.FollowingSeasonActivity\nimport dev.aaa1115910.bv.mobile.activities.FollowingUserActivity\nimport dev.aaa1115910.bv.mobile.activities.HistoryActivity\nimport dev.aaa1115910.bv.mobile.activities.LoginActivity\nimport dev.aaa1115910.bv.mobile.activities.SettingsActivity\nimport dev.aaa1115910.bv.mobile.component.home.UserDialog\nimport dev.aaa1115910.bv.mobile.screen.home.DynamicScreen\nimport dev.aaa1115910.bv.mobile.screen.home.HomeScreen\nimport dev.aaa1115910.bv.mobile.screen.home.SearchScreen\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.swapList\nimport dev.aaa1115910.bv.viewmodel.UserSwitchViewModel\nimport dev.aaa1115910.bv.viewmodel.UserViewModel\nimport dev.aaa1115910.bv.viewmodel.home.PopularViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Composable\nfun MobileMainScreen(\n    modifier: Modifier = Modifier,\n    popularViewModel: PopularViewModel = koinViewModel(),\n    userViewModel: UserViewModel = koinViewModel(),\n    userSwitchViewModel: UserSwitchViewModel = koinViewModel()\n) {\n    val logger = KotlinLogging.logger(\"MobileMainScreen\")\n    val state = rememberMobileMainScreenState(\n        popularViewModel = popularViewModel,\n        userViewModel = userViewModel,\n        userSwitchViewModel = userSwitchViewModel\n    )\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val windowSizeClass = calculateWindowSizeClass(context as Activity)\n\n    val navSuiteType =\n        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(currentWindowAdaptiveInfo())\n\n    val pictures = remember { mutableStateListOf<Picture>() }\n    val previewerState = rememberPreviewerState(\n        verticalDragType = VerticalDragType.UpAndDown,\n        pageCount = { pictures.size },\n        getKey = { pictures[it].key }\n    )\n\n    val onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit =\n        { newPictures, afterSetPictures ->\n            pictures.swapList(newPictures)\n            logger.fInfo { \"update image previewer pictures list: $newPictures\" }\n            afterSetPictures()\n        }\n\n    val verticalNavOrder = listOf(\n        MobileMainScreenNav.Search, MobileMainScreenNav.Home,\n        MobileMainScreenNav.Zone, MobileMainScreenNav.Dynamic\n    ).map { it.name }\n    val horizontalNavOrder = listOf(\n        MobileMainScreenNav.Home, MobileMainScreenNav.Zone,\n        MobileMainScreenNav.Search, MobileMainScreenNav.Dynamic,\n    ).map { it.name }\n\n    val compareNavIndex: (String?, String?) -> Boolean = { a, b ->\n        if (navSuiteType == NavigationSuiteType.NavigationBar) {\n            horizontalNavOrder.indexOf(a) < horizontalNavOrder.indexOf(b)\n        } else {\n            verticalNavOrder.indexOf(a) < verticalNavOrder.indexOf(b)\n        }\n    }\n\n    val navEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition =\n        {\n            val coefficient = 10\n            if (navSuiteType == NavigationSuiteType.NavigationBar) {\n                if (compareNavIndex(\n                        targetState.destination.route,\n                        initialState.destination.route\n                    )\n                ) {\n                    fadeIn() + slideInHorizontally { -it / coefficient }\n                } else {\n                    fadeIn() + slideInHorizontally { it / coefficient }\n                }\n            } else {\n                if (compareNavIndex(\n                        targetState.destination.route,\n                        initialState.destination.route\n                    )\n                ) {\n                    fadeIn() + slideInVertically { -it / coefficient }\n                } else {\n                    fadeIn() + slideInVertically { it / coefficient }\n                }\n            }\n        }\n\n    val navExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition =\n        {\n            val coefficient = 10\n            if (navSuiteType == NavigationSuiteType.NavigationBar) {\n                if (compareNavIndex(\n                        targetState.destination.route,\n                        initialState.destination.route\n                    )\n                ) {\n                    fadeOut() + slideOutHorizontally { it / coefficient }\n                } else {\n                    fadeOut() + slideOutHorizontally { -it / coefficient }\n                }\n            } else {\n                if (compareNavIndex(\n                        targetState.destination.route,\n                        initialState.destination.route\n                    )\n                ) {\n                    fadeOut() + slideOutVertically { it / coefficient }\n                } else {\n                    fadeOut() + slideOutVertically { -it / coefficient }\n                }\n            }\n        }\n\n    BackHandler(previewerState.canClose || previewerState.animating) {\n        if (previewerState.canClose) scope.launch {\n            previewerState.closeTransform()\n        }\n    }\n\n    BackHandler(state.showUserDialog) {\n        state.hideUserDialog()\n    }\n\n    Box(\n        modifier = modifier,\n    ) {\n        NavigationSuiteScaffoldLayout(\n            navigationSuite = {\n                NavigationSuit(\n                    mobileMainScreenState = state,\n                    navigationSuiteType = navSuiteType,\n                    avatar = userViewModel.face,\n                    onNavigate = state::navigate,\n                    onShowUserDialog = state::showUserDialog\n                )\n            }\n        ) {\n            NavHost(\n                navController = state.navController,\n                startDestination = MobileMainScreenNav.Home.name,\n                enterTransition = navEnterTransition,\n                exitTransition = navExitTransition\n            ) {\n                composable(MobileMainScreenNav.Home.name) {\n                    HomeScreen(\n                        rcmdGridState = state.rcmdGridState,\n                        popularGridState = state.popularGridState,\n                        windowSize = state.windowSizeClass.widthSizeClass,\n                        onShowUserDialog = state::showUserDialog\n                    )\n                }\n\n                composable(MobileMainScreenNav.Dynamic.name) {\n                    BackHandler(previewerState.canClose || previewerState.animating) {\n                        if (previewerState.canClose) scope.launch {\n                            previewerState.closeTransform()\n                        }\n                    }\n\n                    DynamicScreen(\n                        dynamicGridState = state.dynamicGridState,\n                        previewerState = previewerState,\n                        onShowPreviewer = onShowPreviewer,\n                        // dynamicViewModel = dynamicViewModel\n                    )\n                }\n\n                composable(MobileMainScreenNav.Search.name) {\n                    SearchScreen()\n                }\n                composable(MobileMainScreenNav.Zone.name) {\n                    DevelopingTipContent()\n                }\n            }\n        }\n    }\n\n    ImagePreviewer(\n        modifier = Modifier\n            .fillMaxSize(),\n        state = previewerState,\n        imageLoader = { index ->\n            val imageRequest = ImageRequest.Builder(LocalContext.current)\n                .data(pictures[index].url)\n                .size(Size.ORIGINAL)\n                .build()\n            rememberAsyncImagePainter(imageRequest)\n        }\n    )\n\n    UserDialog(\n        show = state.showUserDialog,\n        windowWidthSizeClass = windowSizeClass.widthSizeClass,\n        onHideDialog = { state.showUserDialog = false },\n        currentUser = userSwitchViewModel.currentUser.takeIf { it.id != -1 },\n        userList = userSwitchViewModel.userDbList,\n        onSwitchUser = { user ->\n            scope.launch(Dispatchers.IO) {\n                userSwitchViewModel.switchUser(user)\n            }\n        },\n        onAddUser = { context.startActivity(Intent(context, LoginActivity::class.java)) },\n        onDeleteUser = { user ->\n            scope.launch(Dispatchers.IO) {\n                userSwitchViewModel.deleteUser(user)\n            }\n        },\n        onOpenFollowingUser = {\n            context.startActivity(\n                Intent(context, FollowingUserActivity::class.java)\n            )\n        },\n        onOpenHistory = {\n            context.startActivity(\n                Intent(context, HistoryActivity::class.java)\n            )\n        },\n        onOpenFavorite = {\n            context.startActivity(\n                Intent(context, FavoriteActivity::class.java)\n            )\n        },\n        onOpenFollowingPgc = {\n            context.startActivity(\n                Intent(context, FollowingSeasonActivity::class.java)\n            )\n        },\n        onOpenToView = {},\n        onOpenSettings = { context.startActivity(Intent(context, SettingsActivity::class.java)) }\n    )\n}\n\n@Composable\nprivate fun NavigationSuit(\n    modifier: Modifier = Modifier,\n    mobileMainScreenState: MobileMainScreenState,\n    navigationSuiteType: NavigationSuiteType,\n    avatar: String,\n    onNavigate: (MobileMainScreenNav) -> Unit,\n    onShowUserDialog: () -> Unit,\n) {\n    when (navigationSuiteType) {\n        NavigationSuiteType.NavigationBar -> {\n            NavigationSuite(\n                modifier = modifier\n            ) {\n                listOf(\n                    MobileMainScreenNav.Home,\n                    MobileMainScreenNav.Zone,\n                    MobileMainScreenNav.Search,\n                    MobileMainScreenNav.Dynamic,\n                ).forEach { navItem ->\n                    item(\n                        icon = { Icon(navItem.icon, contentDescription = navItem.displayName) },\n                        label = { Text(navItem.displayName) },\n                        selected = mobileMainScreenState.currentNavItem == navItem,\n                        onClick = { onNavigate(navItem) }\n                    )\n                }\n            }\n        }\n\n        NavigationSuiteType.NavigationRail -> {\n            NavigationRail(\n                modifier = modifier,\n                containerColor = MaterialTheme.colorScheme.surfaceContainer\n            ) {\n                NavigationRailItem(\n                    icon = {\n                        if (avatar.isBlank()) {\n                            Icon(Icons.Rounded.Person, contentDescription = \"User Avatar\")\n                        } else {\n                            Box(\n                                modifier = Modifier\n                                    .clip(CircleShape)\n                                    .background(Color.Gray)\n                            ) {\n                                AsyncImage(\n                                    modifier = Modifier\n                                        .size(36.dp),\n                                    model = avatar,\n                                    contentDescription = null,\n                                    contentScale = ContentScale.Crop\n                                )\n                            }\n                        }\n                    },\n                    selected = false,\n                    onClick = onShowUserDialog\n                )\n                Spacer(Modifier.weight(1f))\n                listOf(\n                    MobileMainScreenNav.Search,\n                    MobileMainScreenNav.Home,\n                    MobileMainScreenNav.Zone,\n                    MobileMainScreenNav.Dynamic,\n                ).forEach { navItem ->\n                    NavigationRailItem(\n                        icon = { Icon(navItem.icon, contentDescription = navItem.displayName) },\n                        label = { Text(navItem.displayName) },\n                        selected = mobileMainScreenState.currentNavItem == navItem,\n                        onClick = { onNavigate(navItem) }\n                    )\n                }\n                Spacer(Modifier.weight(1f))\n                listOf(MobileMainScreenNav.Setting).forEach { navItem ->\n                    NavigationRailItem(\n                        icon = { Icon(navItem.icon, contentDescription = navItem.displayName) },\n                        label = { Text(navItem.displayName) },\n                        selected = mobileMainScreenState.currentNavItem == navItem,\n                        onClick = { onNavigate(navItem) }\n                    )\n                }\n            }\n        }\n    }\n}\n\ndata class MobileMainScreenState(\n    val context: Context,\n    val scope: CoroutineScope,\n    val windowSizeClass: WindowSizeClass,\n    //val drawerState: DrawerState,\n    val rcmdGridState: LazyGridState,\n    val popularGridState: LazyGridState,\n    val dynamicGridState: LazyStaggeredGridState,\n    val navController: NavHostController,\n    val currentBackStackEntry: NavBackStackEntry?,\n    val currentNavItem: MobileMainScreenNav,\n    private val homeViewModel: PopularViewModel,\n    private val userViewModel: UserViewModel,\n    private val userSwitchViewModel: UserSwitchViewModel,\n) {\n    companion object {\n        val logger = KotlinLogging.logger {}\n    }\n\n    var activeSearch by mutableStateOf(false)\n\n    var showUserDialog by mutableStateOf(false)\n\n    fun navigate(navItem: MobileMainScreenNav) {\n        logger.fInfo { \"Navigate to ${navItem.name}\" }\n\n        val navigateToRoute: () -> Unit = {\n            val route = navItem.name\n            navController.navigate(route) {\n                launchSingleTop = true\n                popUpTo(navController.graph.findStartDestination().id) {\n                    inclusive = false\n                    saveState = true\n                }\n                restoreState = true\n            }\n        }\n\n        val notCurrentNavItem = currentNavItem != navItem\n\n        when (navItem) {\n            MobileMainScreenNav.Home -> {\n                if (notCurrentNavItem) {\n                    navigateToRoute()\n                } else {\n                    scope.launch { rcmdGridState.animateScrollToItem(0) }\n                    scope.launch { popularGridState.animateScrollToItem(0) }\n                }\n            }\n\n            MobileMainScreenNav.Search -> {\n                if (notCurrentNavItem) {\n                    navigateToRoute()\n                }\n            }\n\n            MobileMainScreenNav.Setting -> {\n                context.startActivity(Intent(context, SettingsActivity::class.java))\n            }\n\n            MobileMainScreenNav.Dynamic -> {\n                if (notCurrentNavItem) {\n                    navigateToRoute()\n                } else {\n                    scope.launch { dynamicGridState.animateScrollToItem(0) }\n                }\n            }\n\n            MobileMainScreenNav.Zone -> {\n                if (notCurrentNavItem) {\n                    navigateToRoute()\n                }\n            }\n        }\n\n        @SuppressLint(\"RestrictedApi\")\n        val breadcrumb = navController\n            .currentBackStack\n            .value\n            .map { it.destination }\n            .filterNot { it is NavGraph }\n            .joinToString(\" > \") { it.route ?: \"null\" }\n        logger.fInfo { \"Navigation Stack: > $breadcrumb\" }\n    }\n\n    fun showUserDialog() {\n        scope.launch(Dispatchers.IO) {\n            userSwitchViewModel.updateUserDbList()\n            this@MobileMainScreenState.showUserDialog = true\n        }\n    }\n\n    fun hideUserDialog() {\n        this@MobileMainScreenState.showUserDialog = false\n    }\n}\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Composable\nfun rememberMobileMainScreenState(\n    context: Context = LocalContext.current,\n    scope: CoroutineScope = rememberCoroutineScope(),\n    windowSizeClass: WindowSizeClass = calculateWindowSizeClass(context as Activity),\n    drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),\n    rcmdGridState: LazyGridState = rememberLazyGridState(),\n    popularGridState: LazyGridState = rememberLazyGridState(),\n    dynamicGridState: LazyStaggeredGridState = rememberLazyStaggeredGridState(),\n    navController: NavHostController = rememberNavController(),\n    popularViewModel: PopularViewModel,//= koinNavViewModel(),\n    userViewModel: UserViewModel,//= koinNavViewModel(),\n    userSwitchViewModel: UserSwitchViewModel //= koinNavViewModel()\n): MobileMainScreenState {\n    val lifecycleOwner = LocalLifecycleOwner.current\n\n    val currentBackStackEntry by navController.currentBackStackEntryAsState()\n    val currentNavItem by remember {\n        derivedStateOf {\n            MobileMainScreenNav.fromName(currentBackStackEntry?.destination?.route ?: \"\")\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        if (popularViewModel.popularVideoList.isNotEmpty()) {\n            scope.launch(Dispatchers.IO) { popularViewModel.loadMore() }\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        userViewModel.updateUserInfo()\n    }\n\n    DisposableEffect(lifecycleOwner) {\n        var leaveFromThisPage = false\n        val observer = LifecycleEventObserver { _, event ->\n            if (event == Lifecycle.Event.ON_PAUSE) {\n                leaveFromThisPage = true\n            } else if (event == Lifecycle.Event.ON_RESUME) {\n                if (leaveFromThisPage) {\n                    scope.launch(Dispatchers.IO) {\n                        userSwitchViewModel.updateUserDbList()\n                    }\n                }\n                leaveFromThisPage = false\n            }\n        }\n\n        lifecycleOwner.lifecycle.addObserver(observer)\n\n        onDispose {\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n\n    return remember(\n        context,\n        scope,\n        windowSizeClass,\n        drawerState,\n        rcmdGridState,\n        popularGridState,\n        dynamicGridState,\n        navController,\n        currentNavItem\n    ) {\n        MobileMainScreenState(\n            context,\n            scope,\n            windowSizeClass,\n            //drawerState,\n            rcmdGridState,\n            popularGridState,\n            dynamicGridState,\n            navController,\n            currentBackStackEntry,\n            currentNavItem,\n            popularViewModel,\n            userViewModel,\n            userSwitchViewModel\n        )\n    }\n}\n\nenum class MobileMainScreenNav(val displayName: String, val icon: ImageVector) {\n    Home(\"首页\", Icons.Rounded.Home),\n    Zone(\"分区\", Icons.AutoMirrored.Rounded.Segment),\n    Search(\"搜索\", Icons.Rounded.Search),\n    Dynamic(\"动态\", Icons.Rounded.FiberNew),\n    Setting(\"设置\", Icons.Rounded.Settings), ;\n\n    companion object {\n        fun fromName(name: String) = entries.firstOrNull { it.name == name } ?: Home\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/QrTokenResultScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.content.res.Configuration\nimport android.net.Uri\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeTopAppBar\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.entity.AuthData\nimport dev.aaa1115910.bv.entity.BvScheme\nimport dev.aaa1115910.bv.mobile.R\nimport dev.aaa1115910.bv.mobile.component.user.UserAvatar\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.repository.UserRepository\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.compose.koinInject\n\n@Composable\nfun QrTokenResultScreen(\n    modifier: Modifier = Modifier,\n    userRepository: UserRepository = koinInject()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger { }\n\n    var parsing by remember { mutableStateOf(true) }\n    var authData by remember { mutableStateOf<AuthData?>(null) }\n    var error by remember { mutableStateOf<Throwable?>(null) }\n    var uid by remember { mutableLongStateOf(-1L) }\n    var username by remember { mutableStateOf<String>(\"\") }\n    var avatar by remember { mutableStateOf<String>(\"\") }\n    var addingUser by remember { mutableStateOf(false) }\n\n    val onBack: () -> Unit = {\n        (context as Activity).finish()\n    }\n\n    val onConfirm: () -> Unit = {\n        if (!addingUser && authData != null) {\n            addingUser = true\n            scope.launch(Dispatchers.IO) {\n                runCatching {\n                    userRepository.addUser(authData!!)\n                }.onFailure {\n                    logger.error(it) { \"Failed to save auth data to prefs\" }\n                    withContext(Dispatchers.Main) {\n                        it.message?.toast(context)\n                    }\n                }.onSuccess {\n                    withContext(Dispatchers.Main) {\n                        R.string.qr_token_result_toast_add_success.toast(context)\n                    }\n                    (context as Activity).finish()\n                    context.startActivity(\n                        context.packageManager.getLaunchIntentForPackage(context.packageName)\n                            ?.apply {\n                                flags =\n                                    Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK\n                            }\n                    )\n                }\n            }\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        runCatching {\n            val uri = (context as Activity).intent.getParcelableExtra<Uri>(\"uri\")\n                ?: throw IllegalArgumentException(\"Uri not found in intent extras\")\n            val data = BvScheme.QrToken.fromUri(uri)\n                ?: throw IllegalArgumentException(\"Invalid QR token URI: $uri\")\n            val qrToken = data as BvScheme.QrToken\n\n            authData = AuthData.fromJson(qrToken.auth)\n            uid = qrToken.uid\n            username = qrToken.username\n            avatar = qrToken.avatar\n        }.onFailure {\n            logger.warn(it) { \"Failed to parse QR token result\" }\n            error = it\n        }\n        parsing = false\n    }\n\n    QrTokenResultContent(\n        modifier = modifier,\n        authData = authData,\n        uid = uid,\n        username = username,\n        avatar = avatar,\n        parsing = parsing,\n        error = error,\n        onBack = onBack,\n        onConfirm = onConfirm\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun QrTokenResultContent(\n    modifier: Modifier = Modifier,\n    authData: AuthData?,\n    uid: Long,\n    username: String,\n    avatar: String,\n    parsing: Boolean,\n    error: Throwable?,\n    onBack: () -> Unit,\n    onConfirm: () -> Unit\n) {\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            LargeTopAppBar(\n                title = {\n                    Text(text = stringResource(R.string.title_mobile_activity_qr_token_result))\n                },\n                navigationIcon = {\n                    IconButton(onClick = onBack) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                            contentDescription = null\n                        )\n                    }\n                }\n            )\n        }\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize()\n        ) {\n            if (parsing) {\n                LoadingIndicator(\n                    modifier = Modifier\n                        .align(Alignment.Center)\n                        .size(80.dp)\n                )\n            } else if (error != null) {\n                Box {\n                    Column(\n                        modifier = Modifier\n                            .verticalScroll(rememberScrollState())\n                    ) {\n                        Text(text = error.stackTraceToString())\n                    }\n                }\n            } else if (authData != null) {\n                Box(\n                    modifier = Modifier.fillMaxSize()\n                ) {\n                    Column(\n                        modifier = Modifier\n                            .padding(top = 24.dp)\n                            .align(Alignment.TopCenter),\n                        horizontalAlignment = Alignment.CenterHorizontally\n                    ) {\n                        UserAvatar(\n                            modifier = Modifier.padding(vertical = 24.dp),\n                            avatar = avatar\n                        )\n                        Text(\n                            text = username,\n                            style = MaterialTheme.typography.titleMedium\n                        )\n                        Text(\n                            text = \"$uid\",\n                            style = MaterialTheme.typography.bodySmall,\n                        )\n                    }\n                    FilledTonalButton(\n                        modifier = Modifier\n                            .align(Alignment.BottomCenter)\n                            .padding(24.dp)\n                            .fillMaxWidth(),\n                        onClick = onConfirm\n                    ) {\n                        Text(text = stringResource(R.string.qr_token_result_button_add_user))\n                    }\n                }\n            } else {\n                Text(\"unknown error\")\n            }\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun QrTokenResultContentParsingPreview() {\n    BVMobileTheme {\n        QrTokenResultContent(\n            authData = null,\n            uid = -1L,\n            username = \"\",\n            avatar = \"\",\n            parsing = true,\n            error = null,\n            onBack = {},\n            onConfirm = {}\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun QrTokenResultContentPreview() {\n    BVMobileTheme {\n        QrTokenResultContent(\n            authData = AuthData(\n                uid = 123456789L,\n                uidCkMd5 = \"exampleUidCkMd5\",\n                sid = \"exampleSid\",\n                sessData = \"exampleSessData\",\n                biliJct = \"exampleBiliJct\",\n                tokenExpiredData = 1728000000L, // Example timestamp\n                accessToken = \"exampleAccessToken\",\n                refreshToken = \"exampleRefreshToken\"\n            ),\n            uid = 3252351L,\n            username = \"bishi\",\n            avatar = \"\",\n            parsing = false,\n            error = null,\n            onBack = {},\n            onConfirm = {}\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun QrTokenResultContentErrorPreview() {\n    BVMobileTheme {\n        QrTokenResultContent(\n            authData = AuthData(\n                uid = 123456789L,\n                uidCkMd5 = \"exampleUidCkMd5\",\n                sid = \"exampleSid\",\n                sessData = \"exampleSessData\",\n                biliJct = \"exampleBiliJct\",\n                tokenExpiredData = 1728000000L, // Example timestamp\n                accessToken = \"exampleAccessToken\",\n                refreshToken = \"exampleRefreshToken\"\n            ),\n            uid = -1L,\n            username = \"\",\n            avatar = \"\",\n            parsing = false,\n            error = IllegalStateException(\"An error occurred while parsing the QR token result\"),\n            onBack = {},\n            onConfirm = {}\n        )\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/RegionBlockScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.WarningAmber\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport kotlin.system.exitProcess\n\n@Composable\nfun RegionBlockScreen(modifier: Modifier = Modifier) {\n    val context = LocalContext.current\n\n    val exitApp: () -> Unit = {\n        (context as Activity).finish()\n        exitProcess(0)\n    }\n\n    RegionBlockContent(\n        modifier = modifier,\n        onExit = exitApp\n    )\n}\n\n@Composable\nprivate fun RegionBlockContent(\n    modifier: Modifier = Modifier,\n    onExit: () -> Unit\n) {\n    Scaffold(\n        modifier = modifier\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize()\n        ) {\n            Column(\n                modifier = Modifier\n                    .padding(horizontal = 24.dp)\n                    .align(Alignment.Center),\n                horizontalAlignment = Alignment.CenterHorizontally,\n                verticalArrangement = Arrangement.spacedBy(24.dp)\n            ) {\n                Icon(\n                    modifier = Modifier.size(32.dp),\n                    imageVector = Icons.Default.WarningAmber,\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.error\n                )\n                Text(\n                    text = stringResource(R.string.region_block_title),\n                    style = MaterialTheme.typography.titleLarge,\n                    textAlign = TextAlign.Center\n                )\n                Text(\n                    text = stringResource(R.string.region_block_subtitle_mobile),\n                    style = MaterialTheme.typography.bodySmall,\n                    textAlign = TextAlign.Center\n                )\n            }\n            FilledTonalButton(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .padding(horizontal = 24.dp, vertical = 12.dp)\n                    .widthIn(max = 320.dp)\n                    .fillMaxWidth(),\n                onClick = onExit\n            ) {\n                Text(text = stringResource(R.string.region_block_exit_button))\n            }\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun RegionBlockScreenPreview() {\n    BVMobileTheme {\n        RegionBlockContent(\n            onExit = {}\n        )\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/UserSpaceScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.app.Activity\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeTopAppBar\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalContext\nimport dev.aaa1115910.bv.mobile.activities.VideoPlayerActivity\nimport dev.aaa1115910.bv.mobile.component.videocard.UpSpaceVideoItem\nimport dev.aaa1115910.bv.viewmodel.user.UserSpaceViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun UserSpaceScreen(\n    modifier: Modifier = Modifier,\n    userSpaceViewModel: UserSpaceViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n\n    val scrollBehavior =\n        TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    LaunchedEffect(Unit) {\n        val intent = (context as Activity).intent\n        if (intent.hasExtra(\"mid\")) {\n            val mid = intent.getLongExtra(\"mid\", 0)\n            val name = intent.getStringExtra(\"name\") ?: \"\"\n            userSpaceViewModel.upMid = mid\n            userSpaceViewModel.upName = name\n            userSpaceViewModel.update()\n        } else {\n            context.finish()\n        }\n    }\n\n    Scaffold(\n        modifier = modifier\n            .fillMaxSize()\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            LargeTopAppBar(\n                title = { Text(text = userSpaceViewModel.upName) },\n                navigationIcon = {\n                    IconButton(onClick = {\n                        (context as Activity).finish()\n                    }) {\n                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)\n                    }\n                },\n                scrollBehavior = scrollBehavior\n            )\n        }\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier.padding(top = innerPadding.calculateTopPadding())\n        ) {\n            items(items = userSpaceViewModel.spaceVideos) { video ->\n                UpSpaceVideoItem(\n                    spaceVideo = video,\n                    onClick = {\n                        VideoPlayerActivity.actionStart(\n                            context = context,\n                            aid = video.aid\n                        )\n                    }\n                )\n            }\n            item {\n                Spacer(modifier = Modifier.navigationBarsPadding())\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/VideoPlayerScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.pm.ActivityInfo\nimport android.view.WindowManager\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.CornerSize\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material.pullrefresh.PullRefreshIndicator\nimport androidx.compose.material.pullrefresh.pullRefresh\nimport androidx.compose.material.pullrefresh.rememberPullRefreshState\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Tab\nimport androidx.compose.material3.TabRow\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.rememberBottomSheetScaffoldState\nimport androidx.compose.material3.windowsizeclass.WindowHeightSizeClass\nimport androidx.compose.material3.windowsizeclass.WindowSizeClass\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.zIndex\nimport coil.compose.AsyncImage\nimport coil.compose.rememberAsyncImagePainter\nimport coil.request.ImageRequest\nimport com.google.accompanist.systemuicontroller.rememberSystemUiController\nimport com.origeek.imageViewer.previewer.ImagePreviewer\nimport com.origeek.imageViewer.previewer.ImagePreviewerState\nimport com.origeek.imageViewer.previewer.VerticalDragType\nimport com.origeek.imageViewer.previewer.rememberPreviewerState\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.biliapi.entity.reply.CommentSort\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.mobile.activities.VideoPlayerActivity\nimport dev.aaa1115910.bv.mobile.component.player.VideoPlayerPages\nimport dev.aaa1115910.bv.mobile.component.reply.CommentItem\nimport dev.aaa1115910.bv.mobile.component.reply.ReplySheetScaffold\nimport dev.aaa1115910.bv.mobile.component.videocard.RelatedVideoItem\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerDanmakuMasksData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerHistoryData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerLoadStateData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerLogsData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerPaymentData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekThumbData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerVideoInfoData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerVideoShotData\nimport dev.aaa1115910.bv.player.entity.VideoListPart\nimport dev.aaa1115910.bv.player.entity.VideoListPgcEpisode\nimport dev.aaa1115910.bv.player.entity.VideoListUgcEpisode\nimport dev.aaa1115910.bv.player.entity.VideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerDanmakuMasksData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerHistoryData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerLoadStateData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerLogsData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerPaymentData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerSeekThumbData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerVideoInfoData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerVideoShotData\nimport dev.aaa1115910.bv.player.mobile.BvPlayer\nimport dev.aaa1115910.bv.player.danmaku.DanmakuView\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.formatPubTimeString\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.swapList\nimport dev.aaa1115910.bv.viewmodel.CommentViewModel\nimport dev.aaa1115910.bv.viewmodel.SeasonViewModel\nimport dev.aaa1115910.bv.viewmodel.VideoPlayerV3ViewModel\nimport dev.aaa1115910.bv.viewmodel.video.VideoDetailViewModel\nimport dev.aaa1115910.bv.viewmodel.login.GeetestResult\nimport com.geetest.sdk.GT3ConfigBean\nimport com.geetest.sdk.GT3ErrorBean\nimport com.geetest.sdk.GT3GeetestUtils\nimport com.geetest.sdk.GT3Listener\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.json.Json\nimport org.json.JSONObject\nimport org.koin.androidx.compose.koinViewModel\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun VideoPlayerScreen(\n    playerViewModel: VideoPlayerV3ViewModel = koinViewModel(),\n    commentVideModel: CommentViewModel = koinViewModel(),\n    seasonVideModel: SeasonViewModel = koinViewModel(),\n    videoDetailViewModel: VideoDetailViewModel = koinViewModel(),\n    windowSizeClass: WindowSizeClass\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val systemUiController = rememberSystemUiController()\n    val logger = KotlinLogging.logger(\"VideoPlayerScreen\")\n\n    // 外部创建 DanmakuView，与 videoPlayer 一致的模式\n    val danmakuView = remember { DanmakuView(context).also { playerViewModel.danmakuView = it } }\n\n    DisposableEffect(danmakuView) {\n        onDispose {\n            danmakuView.release()\n        }\n    }\n\n    var isVideoFullscreen by rememberSaveable { mutableStateOf(false) }\n    val forcePortrait =\n        windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact || windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact\n\n    val pictures = remember { mutableStateListOf<Picture>() }\n    val previewerState = rememberPreviewerState(\n        verticalDragType = VerticalDragType.UpAndDown,\n        pageCount = { pictures.size },\n        getKey = { pictures[it].key }\n    )\n    val replySheetState = rememberBottomSheetScaffoldState()\n\n    // 风控 Geetest 验证\n    var gt3GeetestUtils: GT3GeetestUtils? by remember { mutableStateOf(null) }\n    val gt3ConfigBean by remember { mutableStateOf(GT3ConfigBean()) }\n\n    DisposableEffect(Unit) {\n        gt3GeetestUtils = GT3GeetestUtils(context)\n        gt3ConfigBean.apply {\n            pattern = 1\n            isCanceledOnTouchOutside = false\n            lang = null\n            timeout = 10000\n            webviewTimeout = 10000\n            corners = 24\n            listener = object : GT3Listener() {\n                override fun onReceiveCaptchaCode(p0: Int) {}\n                override fun onStatistics(p0: String?) {}\n                override fun onSuccess(p0: String?) {}\n                override fun onButtonClick() {}\n\n                override fun onClosed(p0: Int) {\n                    playerViewModel.onGeetestCancelled()\n                }\n\n                override fun onFailed(p0: GT3ErrorBean?) {\n                    playerViewModel.onGeetestCancelled()\n                }\n\n                override fun onDialogResult(result: String) {\n                    runCatching {\n                        val geetestResult = Json.decodeFromString<GeetestResult>(result)\n                        gt3GeetestUtils?.showSuccessDialog()\n                        playerViewModel.onGeetestResult(\n                            challenge = geetestResult.geetestChallenge,\n                            validate = geetestResult.geetestValidate,\n                            seccode = geetestResult.geetestSeccode\n                        )\n                    }.onFailure {\n                        gt3GeetestUtils?.showFailedDialog()\n                        playerViewModel.onGeetestCancelled()\n                    }\n                }\n            }\n        }\n        gt3GeetestUtils!!.init(gt3ConfigBean)\n\n        onDispose {\n            gt3GeetestUtils?.destory()\n        }\n    }\n\n    LaunchedEffect(playerViewModel.showGeetestDialog) {\n        if (playerViewModel.showGeetestDialog) {\n            gt3GeetestUtils?.startCustomFlow()\n            gt3ConfigBean.api1Json = JSONObject().apply {\n                put(\"success\", 1)\n                put(\"gt\", playerViewModel.geetestGt)\n                put(\"challenge\", playerViewModel.geetestChallenge)\n            }\n            gt3GeetestUtils?.getGeetest()\n        }\n    }\n\n    val setPreviewerPictures: (List<Picture>, () -> Unit) -> Unit =\n        { newPictures, afterSetPictures ->\n            pictures.clear()\n            pictures.addAll(newPictures)\n            afterSetPictures()\n        }\n\n    SideEffect {\n        systemUiController.isStatusBarVisible = !isVideoFullscreen\n        systemUiController.isNavigationBarVisible = !isVideoFullscreen\n        if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Expanded) {\n            systemUiController.statusBarDarkContentEnabled = true\n            systemUiController.setStatusBarColor(Color.Black)\n        }\n        if (forcePortrait) {\n            if (isVideoFullscreen) {\n                (context as Activity).requestedOrientation =\n                    ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE\n            } else {\n                //在模拟器设为手机尺寸时，横屏时会莫名其妙抛出异常，貌似与折叠屏特性有关，因此手机上强制竖屏\n                //java.lang.IllegalArgumentException: Bounding rectangle must start at the top or left window edge for folding features\n                @SuppressLint(\"SourceLockedOrientationActivity\")\n                (context as Activity).requestedOrientation =\n                    ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT\n            }\n        } else {\n            (context as Activity).requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED\n        }\n    }\n\n    LaunchedEffect(isVideoFullscreen) {\n        if (isVideoFullscreen) {\n            (context as Activity).window.setFlags(\n                WindowManager.LayoutParams.FLAG_FULLSCREEN,\n                WindowManager.LayoutParams.FLAG_FULLSCREEN\n            )\n        } else {\n            (context as Activity).window.clearFlags(\n                WindowManager.LayoutParams.FLAG_FULLSCREEN\n            )\n        }\n    }\n\n    BackHandler(previewerState.canClose || previewerState.animating) {\n        if (previewerState.canClose) scope.launch {\n            previewerState.closeTransform()\n        }\n    }\n\n    BackHandler(isVideoFullscreen) {\n        isVideoFullscreen = false\n    }\n\n    Scaffold(\n        containerColor = if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Expanded) Color.Black else MaterialTheme.colorScheme.surfaceContainer\n    ) { innerPadding ->\n        Row(\n            modifier = Modifier\n                .ifElse(\n                    !isVideoFullscreen,\n                    Modifier.padding(top = innerPadding.calculateTopPadding())\n                )\n            //.padding(top = innerPadding.calculateTopPadding())\n        ) {\n            val leftPartWidth by animateFloatAsState(\n                targetValue = if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded && !isVideoFullscreen) 0.6f else 1f,\n                label = \"VideoPlayerLeftPartWidth\"\n            )\n            Column(\n                modifier = Modifier\n                    .fillMaxWidth(leftPartWidth)\n            ) {\n                if (playerViewModel.videoPlayer != null) {\n                    CompositionLocalProvider(\n                        LocalVideoPlayerSeekThumbData provides VideoPlayerSeekThumbData(\n                            idleIcon = playerViewModel.playerIconIdle,\n                            movingIcon = playerViewModel.playerIconMoving\n                        ),\n                        LocalVideoPlayerVideoInfoData provides VideoPlayerVideoInfoData(\n                            width = playerViewModel.currentVideoWidth,\n                            height = playerViewModel.currentVideoHeight,\n                            codec = playerViewModel.currentVideoCodec.name,\n                            title = playerViewModel.title,\n                            partTitle = playerViewModel.partTitle,\n                        ),\n                        LocalVideoPlayerLogsData provides VideoPlayerLogsData(\n                            logs = playerViewModel.logs\n                        ),\n                        LocalVideoPlayerHistoryData provides VideoPlayerHistoryData(\n                            lastPlayed = playerViewModel.lastPlayed,\n                        ),\n                        LocalVideoPlayerPaymentData provides VideoPlayerPaymentData(\n                            needPay = playerViewModel.needPay,\n                            epid = playerViewModel.epid,\n                            showPreviewTip = playerViewModel.showPreviewTip,\n                        ),\n                        LocalVideoPlayerLoadStateData provides VideoPlayerLoadStateData(\n                            loadState = playerViewModel.loadState,\n                            errorMessage = playerViewModel.errorMessage,\n                        ),\n                        LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n                            availableResolutions = playerViewModel.availableQuality,\n                            availableVideoCodec = playerViewModel.availableVideoCodec,\n                            availableAudio = playerViewModel.availableAudio,\n                            availableSubtitleTracks = playerViewModel.availableSubtitle,\n                            availableVideoList = playerViewModel.availableVideoList,\n                            currentVideoCid = playerViewModel.currentCid,\n                            currentResolution = playerViewModel.currentQuality,\n                            currentVideoCodec = playerViewModel.currentVideoCodec,\n                            currentVideoAspectRatio = playerViewModel.currentVideoAspectRatio,\n                            currentVideoSpeed = playerViewModel.currentPlaySpeed,\n                            currentAudio = playerViewModel.currentAudio,\n                            currentDanmakuEnabled = playerViewModel.currentDanmakuEnabled,\n                            currentDanmakuEnabledList = playerViewModel.currentDanmakuTypes,\n                            currentDanmakuScale = playerViewModel.currentDanmakuScale,\n                            currentDanmakuOpacity = playerViewModel.currentDanmakuOpacity,\n                            currentDanmakuArea = playerViewModel.currentDanmakuArea,\n                            currentDanmakuMask = playerViewModel.currentDanmakuMask,\n                            currentSubtitleId = playerViewModel.currentSubtitleId,\n                            currentSubtitleData = playerViewModel.currentSubtitleData,\n                            currentSubtitleFontSize = playerViewModel.currentSubtitleFontSize,\n                            currentSubtitleBackgroundOpacity = playerViewModel.currentSubtitleBackgroundOpacity,\n                            currentSubtitleBottomPadding = playerViewModel.currentSubtitleBottomPadding,\n                            currentPlayMode = playerViewModel.currentPlayMode,\n                            incognitoMode = Prefs.incognitoMode,\n                            defaultStartPosition = Prefs.playerDefaultStartPosition.toPlayerType()\n                        ),\n                        LocalVideoPlayerDanmakuMasksData provides VideoPlayerDanmakuMasksData(\n                            danmakuMasks = playerViewModel.danmakuMasks,\n                        ),\n                        LocalVideoPlayerVideoShotData provides VideoPlayerVideoShotData(\n                            videoShot = playerViewModel.videoShot,\n                        ),\n                    ) {\n                        BvPlayer(\n                            modifier = if (isVideoFullscreen) Modifier\n                                .fillMaxSize()\n                                .zIndex(1f)\n                            else Modifier\n                                .ifElse(\n                                    { windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded },\n                                    Modifier\n                                        .padding(12.dp, 0.dp, 12.dp, 12.dp)\n                                        .clip(MaterialTheme.shapes.medium)\n                                )\n                                .fillMaxWidth()\n                                .aspectRatio(16f / 9f),\n                            isFullScreen = isVideoFullscreen,\n                            videoPlayer = playerViewModel.videoPlayer!!,\n                            danmakuView = danmakuView,\n                            onClearBackToHistoryData = { playerViewModel.lastPlayed = 0 },\n                            onEnterFullScreen = {\n                                isVideoFullscreen = true\n                            },\n                            onExitFullScreen = {\n                                isVideoFullscreen = false\n                            },\n                            onBack = { (context as Activity).finish() },\n                            onChangeResolution = { code, afterChange ->\n                                scope.launch(Dispatchers.IO) {\n                                    playerViewModel.currentQuality = code\n                                    playerViewModel.playQuality(code)\n                                    afterChange()\n                                }\n                            },\n                            onChangeVideoCodec = { codec, afterChange ->\n                                scope.launch(Dispatchers.IO) {\n                                    playerViewModel.currentVideoCodec = codec\n                                    playerViewModel.playQuality(codec = codec)\n                                    afterChange()\n                                }\n                            },\n                            onChangeAudio = { audio, afterChange ->\n                                scope.launch(Dispatchers.IO) {\n                                    playerViewModel.currentAudio = audio\n                                    playerViewModel.playQuality(audio = audio)\n                                    afterChange()\n                                }\n                            },\n                            onChangeSpeed = { speed ->\n                                playerViewModel.currentPlaySpeed = speed\n                                // Prefs.defaultPlaySpeed = speed\n                            },\n                            onToggleDanmaku = { enabled ->\n                                playerViewModel.currentDanmakuEnabled = enabled\n                                Prefs.defaultDanmakuEnabled = enabled\n                            },\n                            onEnabledDanmakuTypesChange = { types ->\n                                playerViewModel.currentDanmakuTypes.swapList(types)\n                            },\n                            onDanmakuOpacityChange = { opacity ->\n                                playerViewModel.currentDanmakuOpacity = opacity\n                                Prefs.defaultDanmakuOpacity = opacity\n                            },\n                            onDanmakuScaleChange = { scale ->\n                                playerViewModel.currentDanmakuScale = scale\n                                Prefs.defaultDanmakuScale = scale\n                            },\n                            onDanmakuAreaChange = { area ->\n                                playerViewModel.currentDanmakuArea = area\n                                Prefs.defaultDanmakuArea = area\n                            },\n                            onPlayModeChange = { playMode ->\n                                playerViewModel.currentPlayMode = playMode\n                                Prefs.defaultPlayMode = playMode\n                            },\n                            onLoadNextVideo = playerViewModel::playNextVideo,\n                            onLoadNewVideo = { videoListItem ->\n                                logger.fInfo { \"on load new video: $videoListItem\" }\n                                var aid = 0L\n                                var cid = 0L\n                                var epid: Int? = null\n                                var seasonId: Int? = null\n\n                                when (videoListItem) {\n                                    is VideoListPart -> {\n                                        aid = videoListItem.aid\n                                        cid = videoListItem.cid\n                                        epid = videoListItem.epid\n                                        seasonId = videoListItem.seasonId\n                                    }\n\n                                    is VideoListUgcEpisode -> {\n                                        aid = videoListItem.aid\n                                        cid = videoListItem.cid\n                                        epid = videoListItem.epid\n                                        seasonId = videoListItem.seasonId\n                                    }\n\n                                    is VideoListPgcEpisode -> {\n                                        aid = videoListItem.aid\n                                        cid = videoListItem.cid\n                                        epid = videoListItem.epid\n                                        seasonId = videoListItem.seasonId\n                                    }\n                                }\n                                playerViewModel.loadPlayUrl(\n                                    avid = aid,\n                                    cid = cid,\n                                    epid = epid,\n                                    seasonId = seasonId,\n                                    continuePlayNext = true\n                                )\n                            }\n                        )\n                    }\n                }\n                val titles = listOf(\"简介\", \"评论\")\n                val pagerState = rememberPagerState(\n                    initialPage = 0,\n                    initialPageOffsetFraction = 0f,\n                    pageCount = { 2 }\n                )\n                if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Expanded) {\n                    // 小屏幕下的视频详情推荐/评论\n                    ReplySheetScaffold(\n                        aid = commentVideModel.commentId,\n                        rpid = commentVideModel.rpid,\n                        repliesCount = commentVideModel.rpCount,\n                        sheetState = replySheetState,\n                        previewerState = previewerState,\n                        onShowPreviewer = setPreviewerPictures\n                    ) {\n                        Column {\n                            TabRow(\n                                selectedTabIndex = pagerState.currentPage\n                            ) {\n                                titles.forEachIndexed { index, title ->\n                                    Tab(\n                                        selected = pagerState.currentPage == index,\n                                        onClick = { scope.launch { pagerState.scrollToPage(index) } },\n                                        text = {\n                                            Text(\n                                                text = title,\n                                                maxLines = 2,\n                                                overflow = TextOverflow.Ellipsis\n                                            )\n                                        }\n                                    )\n                                }\n                            }\n                            HorizontalPager(\n                                state = pagerState\n                            ) { page ->\n                                when (page) {\n                                    0 -> {\n                                        LazyColumn(\n                                            modifier = Modifier.fillMaxSize()\n                                        ) {\n                                            item {\n                                                VideoPlayerInfo(\n                                                    modifier = Modifier.padding(12.dp),\n                                                    upAvatar = videoDetailViewModel.videoDetail?.author?.face\n                                                        ?: \"\",\n                                                    upName = videoDetailViewModel.videoDetail?.author?.name\n                                                        ?: \"\",\n                                                    upFansCount = 0,\n                                                    title = videoDetailViewModel.videoDetail?.title\n                                                        ?: \"\",\n                                                    description = videoDetailViewModel.videoDetail?.description\n                                                        ?: \"\",\n                                                    playCount = videoDetailViewModel.videoDetail?.stat?.view\n                                                        ?: 0,\n                                                    danmakuCount = videoDetailViewModel.videoDetail?.stat?.danmaku\n                                                        ?: 0,\n                                                    date = videoDetailViewModel.videoDetail?.publishDate\n                                                        ?.formatPubTimeString(context) ?: \"\",\n                                                    avid = videoDetailViewModel.videoDetail?.aid\n                                                        ?: 0\n                                                )\n                                            }\n                                            item {\n                                                VideoPlayerPages(\n                                                    currentCid = playerViewModel.currentCid,\n                                                    pages = videoDetailViewModel.videoDetail?.pages\n                                                        ?: emptyList(),\n                                                    ugcSeason = videoDetailViewModel.videoDetail?.ugcSeason,\n                                                    pgcSections = seasonVideModel.seasonData?.sections\n                                                        ?: emptyList(),\n                                                    onClickPage = { videoPage ->\n                                                        playerViewModel.loadPlayUrl(\n                                                            avid = videoDetailViewModel.videoDetail!!.aid,\n                                                            cid = videoPage.cid,\n                                                            continuePlayNext = true\n                                                        )\n                                                    },\n                                                    onClickEpisode = { sectionIndex, episode ->\n                                                        videoDetailViewModel.updateUgcSeasonSectionVideoList(\n                                                            sectionIndex\n                                                        )\n                                                        playerViewModel.loadPlayUrl(\n                                                            avid = episode.aid,\n                                                            cid = episode.cid,\n                                                            epid = episode.epid,\n                                                            continuePlayNext = true\n                                                        )\n                                                    }\n                                                )\n                                            }\n                                            items(\n                                                items = videoDetailViewModel.videoDetail?.relatedVideos\n                                                    ?: emptyList()\n                                            ) { relatedVideo ->\n                                                RelatedVideoItem(\n                                                    relatedVideo = relatedVideo,\n                                                    onClick = {\n                                                        VideoPlayerActivity.actionStart(\n                                                            context = context,\n                                                            aid = relatedVideo.aid,\n                                                            fromSeason = relatedVideo.jumpToSeason\n                                                        )\n                                                    }\n                                                )\n                                            }\n                                            item {\n                                                Spacer(modifier = Modifier.navigationBarsPadding())\n                                            }\n                                        }\n                                    }\n\n                                    1 -> {\n                                        VideoComments(\n                                            previewerState = previewerState,\n                                            comments = commentVideModel.comments,\n                                            commentSort = commentVideModel.commentSort,\n                                            refreshingComments = commentVideModel.refreshingComments,\n                                            onLoadMoreComments = {\n                                                scope.launch(Dispatchers.IO) { commentVideModel.loadMoreComment() }\n                                            },\n                                            onRefreshComments = {\n                                                scope.launch(Dispatchers.IO) { commentVideModel.refreshComments() }\n                                            },\n                                            onSwitchCommentSort = {\n                                                scope.launch(Dispatchers.IO) {\n                                                    commentVideModel.switchCommentSort(it)\n                                                }\n                                            },\n                                            onShowPreviewer = setPreviewerPictures,\n                                            onShowReplies = { rpId, repliesCount ->\n                                                //logger.info { \"show reply sheet: rpid=$replyId\" }\n                                                commentVideModel.rpid = rpId\n                                                commentVideModel.rpCount = repliesCount\n                                                scope.launch { replySheetState.bottomSheetState.expand() }\n                                            }\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                } else {\n                    // 大屏幕下视频下方的视频详情和推荐视频\n                    LazyColumn(\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .padding(horizontal = 12.dp)\n                    ) {\n                        item {\n                            VideoPlayerInfo(\n                                modifier = Modifier.padding(12.dp),\n                                upAvatar = videoDetailViewModel.videoDetail?.author?.face\n                                    ?: \"\",\n                                upName = videoDetailViewModel.videoDetail?.author?.name ?: \"\",\n                                upFansCount = 0,\n                                title = videoDetailViewModel.videoDetail?.title ?: \"\",\n                                description = videoDetailViewModel.videoDetail?.description\n                                    ?: \"\",\n                                playCount = videoDetailViewModel.videoDetail?.stat?.view ?: 0,\n                                danmakuCount = videoDetailViewModel.videoDetail?.stat?.danmaku\n                                    ?: 0,\n                                date = videoDetailViewModel.videoDetail?.publishDate\n                                    ?.formatPubTimeString(context) ?: \"\",\n                                avid = videoDetailViewModel.videoDetail?.aid ?: 0,\n                                backgroundColor = MaterialTheme.colorScheme.surfaceContainer\n                            )\n                        }\n                        item {\n                            VideoPlayerPages(\n                                modifier = Modifier\n                                    .padding(vertical = 12.dp)\n                                    .clip(MaterialTheme.shapes.medium),\n                                currentCid = playerViewModel.currentCid,\n                                pages = videoDetailViewModel.videoDetail?.pages ?: emptyList(),\n                                ugcSeason = videoDetailViewModel.videoDetail?.ugcSeason,\n                                pgcSections = seasonVideModel.seasonData?.sections ?: emptyList(),\n                                onClickPage = { videoPage ->\n                                    playerViewModel.loadPlayUrl(\n                                        avid = videoDetailViewModel.videoDetail!!.aid,\n                                        cid = videoPage.cid,\n                                        continuePlayNext = true\n                                    )\n                                },\n                                onClickEpisode = { sectionIndex, episode ->\n                                    videoDetailViewModel.updateUgcSeasonSectionVideoList(\n                                        sectionIndex\n                                    )\n                                    playerViewModel.loadPlayUrl(\n                                        avid = episode.aid,\n                                        cid = episode.cid,\n                                        epid = episode.epid,\n                                        continuePlayNext = true\n                                    )\n                                }\n                            )\n                        }\n                        itemsIndexed(\n                            items = videoDetailViewModel.videoDetail?.relatedVideos\n                                ?: emptyList()\n                        ) { index, relatedVideo ->\n                            RelatedVideoItem(\n                                modifier = Modifier\n                                    .ifElse(\n                                        { index == 0 },\n                                        Modifier.clip(\n                                            MaterialTheme.shapes.medium.copy(\n                                                bottomStart = CornerSize(0.dp),\n                                                bottomEnd = CornerSize(0.dp)\n                                            )\n                                        )\n                                    )\n                                    .ifElse(\n                                        {\n                                            index == (videoDetailViewModel.videoDetail?.relatedVideos?.size\n                                                ?: 0) - 1\n                                        },\n                                        Modifier.clip(\n                                            MaterialTheme.shapes.medium.copy(\n                                                topStart = CornerSize(0.dp),\n                                                topEnd = CornerSize(0.dp)\n                                            )\n                                        )\n                                    ),\n                                relatedVideo = relatedVideo,\n                                onClick = {\n                                    VideoPlayerActivity.actionStart(\n                                        context = context,\n                                        aid = relatedVideo.aid,\n                                        fromSeason = relatedVideo.jumpToSeason\n                                    )\n                                }\n                            )\n                        }\n                        item {\n                            Spacer(modifier = Modifier.navigationBarsPadding())\n                        }\n                    }\n                }\n            }\n            if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded) {\n                // 大屏幕下的右侧评论\n                Box(\n                    modifier = Modifier.padding(end = 12.dp)\n                ) {\n                    ReplySheetScaffold(\n                        modifier = Modifier,\n                        aid = commentVideModel.commentId,\n                        rpid = commentVideModel.rpid,\n                        repliesCount = commentVideModel.rpCount,\n                        sheetState = replySheetState,\n                        previewerState = previewerState,\n                        onShowPreviewer = setPreviewerPictures\n                    ) {\n                        VideoComments(\n                            modifier = Modifier.fillMaxWidth(),\n                            previewerState = previewerState,\n                            comments = commentVideModel.comments,\n                            commentSort = commentVideModel.commentSort,\n                            refreshingComments = commentVideModel.refreshingComments,\n                            onLoadMoreComments = {\n                                scope.launch(Dispatchers.IO) { commentVideModel.loadMoreComment() }\n                            },\n                            onRefreshComments = {\n                                scope.launch(Dispatchers.IO) { commentVideModel.refreshComments() }\n                            },\n                            onSwitchCommentSort = {\n                                scope.launch(Dispatchers.IO) {\n                                    commentVideModel.switchCommentSort(it)\n                                }\n                            },\n                            onShowPreviewer = setPreviewerPictures,\n                            onShowReplies = { rpId, repliesCount ->\n                                //logger.info { \"show reply sheet: rpid=$replyId\" }\n                                commentVideModel.rpid = rpId\n                                commentVideModel.rpCount = repliesCount\n                                scope.launch { replySheetState.bottomSheetState.expand() }\n                            }\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    ImagePreviewer(\n        modifier = Modifier\n            .fillMaxSize(),\n        state = previewerState,\n        imageLoader = { index ->\n            val imageRequest = ImageRequest.Builder(LocalContext.current)\n                .data(pictures[index].url)\n                .size(coil.size.Size.ORIGINAL)\n                .build()\n            // 获取图片的初始大小\n            rememberAsyncImagePainter(imageRequest)\n            //rememberAsyncImagePainter(pictures[index].url)\n        }\n    )\n}\n\n@Composable\nfun VideoPlayerInfo(\n    modifier: Modifier = Modifier,\n    upAvatar: String,\n    upName: String,\n    upFansCount: Int,\n    title: String,\n    description: String,\n    playCount: Long,\n    danmakuCount: Int,\n    date: String,\n    avid: Long,\n    backgroundColor: Color = MaterialTheme.colorScheme.surface\n) {\n    val summaryTextStyle = MaterialTheme.typography.bodySmall.copy(\n        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n    )\n\n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .background(backgroundColor),\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Row(\n                modifier = Modifier.height(64.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Box(\n                    modifier = Modifier\n                        .padding(0.dp, 8.dp, 8.dp, 8.dp)\n                ) {\n                    AsyncImage(\n                        modifier = Modifier\n                            .size(48.dp)\n                            .clip(CircleShape)\n                            .background(Color.Gray),\n                        model = upAvatar,\n                        contentDescription = null\n                    )\n                }\n\n                Column(\n                    modifier = Modifier\n                        .fillMaxHeight()\n                        .padding(vertical = 12.dp),\n                    verticalArrangement = Arrangement.SpaceEvenly\n                ) {\n                    Text(\n                        text = upName,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        style = MaterialTheme.typography.labelLarge\n                    )\n                    Text(\n                        text = \"$upFansCount\",\n                        style = summaryTextStyle,\n                        fontSize = 10.sp\n                    )\n                }\n            }\n\n            Button(onClick = { /*TODO*/ }) {\n                Text(text = \"Follow\")\n            }\n        }\n        Text(\n            text = title,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis,\n            style = MaterialTheme.typography.titleMedium\n        )\n        ProvideTextStyle(summaryTextStyle) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    Icon(\n                        modifier = Modifier,\n                        painter = painterResource(id = R.drawable.ic_play_count),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                    )\n                    Text(text = \"$playCount\")\n                }\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    Icon(\n                        modifier = Modifier,\n                        painter = painterResource(id = R.drawable.ic_danmaku_count),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                    )\n                    Text(text = \"$danmakuCount\")\n                }\n                Text(text = date)\n                Text(text = \"av$avid\")\n            }\n            Text(text = description)\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun VideoComments(\n    modifier: Modifier = Modifier,\n    previewerState: ImagePreviewerState,\n    comments: List<Comment>,\n    commentSort: CommentSort,\n    refreshingComments: Boolean,\n    onLoadMoreComments: () -> Unit,\n    onRefreshComments: () -> Unit,\n    onSwitchCommentSort: (CommentSort) -> Unit,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit,\n    onShowReplies: (rpId: Long, repliesCount: Int) -> Unit\n) {\n    val listState = rememberLazyListState()\n    val pullRefreshState = rememberPullRefreshState(refreshingComments, { onRefreshComments() })\n\n    val shouldLoadMore by remember {\n        derivedStateOf {\n            val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()\n                ?: return@derivedStateOf true\n\n            lastVisibleItem.index >= listState.layoutInfo.totalItemsCount - 10\n        }\n    }\n\n    LaunchedEffect(shouldLoadMore) {\n        if (shouldLoadMore) onLoadMoreComments()\n    }\n\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n            .pullRefresh(state = pullRefreshState)\n    ) {\n        LazyColumn(\n            state = listState\n        ) {\n            item {\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(start = 16.dp, end = 8.dp),\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Text(\n                        text = when (commentSort) {\n                            CommentSort.Hot -> \"热门评论\"\n                            CommentSort.Time -> \"最新评论\"\n                            else -> \"\"\n                        },\n                        style = MaterialTheme.typography.titleMedium\n                    )\n                    TextButton(onClick = {\n                        onSwitchCommentSort(\n                            when (commentSort) {\n                                CommentSort.Hot -> CommentSort.Time\n                                CommentSort.Time -> CommentSort.Hot\n                                else -> CommentSort.Hot\n                            }\n                        )\n                    }) {\n                        Text(\n                            text = when (commentSort) {\n                                CommentSort.Hot -> \"按热度\"\n                                CommentSort.Time -> \"按时间\"\n                                else -> \"\"\n                            }\n                        )\n                    }\n                }\n            }\n\n            itemsIndexed(items = comments) { index, comment ->\n                Box {\n                    CommentItem(\n                        comment = comment,\n                        previewerState = previewerState,\n                        onShowPreviewer = onShowPreviewer,\n                        onShowReply = { rpId ->\n                            onShowReplies(rpId, comment.repliesCount)\n                        }\n                    )\n                }\n            }\n            item {\n                Spacer(modifier = Modifier.navigationBarsPadding())\n            }\n        }\n        PullRefreshIndicator(\n            refreshingComments,\n            pullRefreshState,\n            Modifier.align(Alignment.TopCenter)\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun VideoPlayerInfoPreview() {\n    BVMobileTheme {\n        Surface {\n            VideoPlayerInfo(\n                modifier = Modifier.padding(24.dp),\n                upAvatar = \"https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg@450w_450h_progressive.webp\",\n                upName = \"bishi\",\n                upFansCount = 1400000000,\n                title = \"This is the video title... repeat, this is the video title.\",\n                description = \"descriptions....descriptions....descriptions....descriptions....descriptions....descriptions....descriptions....descriptions....descriptions....\",\n                playCount = 2434,\n                danmakuCount = 14,\n                date = \"2023-5-22 23:17\",\n                avid = 170001,\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/DynamicScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.home\n\nimport android.app.Activity\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState\nimport androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid\nimport androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells\nimport androidx.compose.foundation.lazy.staggeredgrid.items\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport com.origeek.imageViewer.previewer.ImagePreviewerState\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.biliapi.entity.user.DynamicItem\nimport dev.aaa1115910.biliapi.entity.user.DynamicType\nimport dev.aaa1115910.bv.mobile.activities.DynamicDetailActivity\nimport dev.aaa1115910.bv.mobile.activities.VideoPlayerActivity\nimport dev.aaa1115910.bv.mobile.component.home.dynamic.DynamicItem\nimport dev.aaa1115910.bv.util.OnBottomReached\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.getLane\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.home.DynamicViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3WindowSizeClassApi::class)\n@Composable\nfun DynamicScreen(\n    modifier: Modifier = Modifier,\n    dynamicViewModel: DynamicViewModel = koinViewModel(),\n    dynamicGridState: LazyStaggeredGridState,\n    previewerState: ImagePreviewerState,\n    onShowPreviewer: (newPictures: List<Picture>, afterSetPictures: () -> Unit) -> Unit\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger { }\n    val windowSize = calculateWindowSizeClass(context as Activity).widthSizeClass\n\n    val lane by remember { derivedStateOf { dynamicGridState.getLane() } }\n\n    val onClickDynamicItem: (DynamicItem) -> Unit = { dynamicItem ->\n        logger.fInfo { \"click dynamic type: ${dynamicItem.type}\" }\n        when (dynamicItem.type) {\n            DynamicType.Av -> {\n                println(\"=== ${dynamicItem.video} ===\")\n                VideoPlayerActivity.actionStart(\n                    context = context,\n                    aid = dynamicItem.video!!.aid,\n                    fromSeason = dynamicItem.video!!.seasonId != null\n                            && dynamicItem.video!!.seasonId != 0,\n                )\n            }\n\n            DynamicType.Pgc -> {\n                VideoPlayerActivity.actionStart(\n                    context = context,\n                    //aid = dynamicItem.pgc!!.epid,\n                    aid = 0,\n                    fromSeason = true,\n                    epid = dynamicItem.pgc!!.epid,\n                    seasonId = dynamicItem.pgc!!.seasonId,\n                )\n            }\n\n            else -> {\n                if (dynamicItem.id != null) {\n                    DynamicDetailActivity.actionStart(context, dynamicItem.id!!)\n                } else {\n                    \"原动态不存在\".toast(context)\n                }\n            }\n        }\n    }\n\n    dynamicGridState.OnBottomReached(\n        loading = dynamicViewModel.loadingAll\n    ) {\n        logger.fInfo { \"on reached rcmd page bottom\" }\n        scope.launch(Dispatchers.IO) {\n            dynamicViewModel.loadMoreAll()\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            TopAppBar(\n                title = { Text(text = \"Dynamic\") },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainer,\n                )\n            )\n        },\n        containerColor = MaterialTheme.colorScheme.surfaceContainer\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier.padding(top = innerPadding.calculateTopPadding())\n        ) {\n            LazyVerticalStaggeredGrid(\n                modifier = modifier\n                    .fillMaxSize()\n                    .ifElse(\n                        { windowSize != WindowWidthSizeClass.Compact },\n                        Modifier.clip(MaterialTheme.shapes.medium)\n                    )\n                    .background(MaterialTheme.colorScheme.surface),\n                columns = StaggeredGridCells.Adaptive(300.dp),\n                state = dynamicGridState,\n                verticalItemSpacing = 8.dp,\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n                contentPadding = PaddingValues(if (lane == 1) 0.dp else 8.dp)\n            ) {\n                items(items = dynamicViewModel.dynamicAllList) { dynamicItem ->\n                    DynamicItem(\n                        modifier = Modifier\n                            .ifElse(lane != 1, Modifier.clip(MaterialTheme.shapes.medium)),\n                        dynamicItem = dynamicItem,\n                        previewerState = previewerState,\n                        onShowPreviewer = onShowPreviewer,\n                        onClick = onClickDynamicItem\n                    )\n                }\n            }\n        }\n    }\n}\n\nprivate val exampleAuthorData = DynamicItem.DynamicAuthorModule(\n    author = \"author\",\n    avatar = \"\",\n    mid = 0,\n    pubTime = \"54 分钟前 投稿了视频\",\n    pubAction = \"\"\n)\n\nprivate val exampleFooterData = DynamicItem.DynamicFooterModule(\n    like = 2,\n    comment = 61,\n    share = 8,\n)\n\nprivate val exampleVideoData = DynamicItem.DynamicVideoModule(\n    aid = 0,\n    title = \"title\",\n    cover = \"\",\n    duration = \"23:45\",\n    play = \"xx play\",\n    danmaku = \"xx dm\",\n    seasonId = 0,\n    cid = 0,\n    text = \"desc\"\n)\n\nprivate val exampleDynamicItemData = DynamicItem(\n    type = DynamicType.Av,\n    author = exampleAuthorData,\n    video = exampleVideoData,\n    footer = exampleFooterData\n)\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/HomeScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.home\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.PagerState\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Person\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Tab\nimport androidx.compose.material3.TabRow\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.zIndex\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.mobile.activities.VideoPlayerActivity\nimport dev.aaa1115910.bv.mobile.component.home.HomeTab\nimport dev.aaa1115910.bv.mobile.screen.home.home.PopularPage\nimport dev.aaa1115910.bv.mobile.screen.home.home.RcmdPage\nimport dev.aaa1115910.bv.viewmodel.UserViewModel\nimport dev.aaa1115910.bv.viewmodel.home.PopularViewModel\nimport dev.aaa1115910.bv.viewmodel.home.RecommendViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\nimport kotlin.Int\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun HomeScreen(\n    modifier: Modifier = Modifier,\n    rcmdGridState: LazyGridState,\n    popularGridState: LazyGridState,\n    popularViewModel: PopularViewModel = koinViewModel(),\n    recommendViewModel: RecommendViewModel = koinViewModel(),\n    userViewModel: UserViewModel = koinViewModel(),\n    windowSize: WindowWidthSizeClass,\n    onShowUserDialog: () -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val pageState = rememberPagerState(pageCount = { 2 })\n\n    Scaffold(\n        modifier = modifier,\n        containerColor = MaterialTheme.colorScheme.surfaceContainer,\n        topBar = {\n            HomeTopAppBar(\n                windowSize = windowSize,\n                avatar = userViewModel.face,\n                onShowUserDialog = onShowUserDialog\n            )\n        }\n    ) { innerPadding ->\n        HomeScreenContent(\n            modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),\n            pageState = pageState,\n            selectedTabIndex = pageState.currentPage,\n            windowSize = windowSize,\n            rcmdGridState = rcmdGridState,\n            popularGridState = popularGridState,\n            onChangeTabIndex = { scope.launch { pageState.animateScrollToPage(it) } },\n            popularViewModel = popularViewModel,\n            recommendViewModel = recommendViewModel,\n        )\n    }\n}\n\n@Composable\nfun HomeScreenContent(\n    modifier: Modifier = Modifier,\n    pageState: PagerState,\n    selectedTabIndex: Int,\n    windowSize: WindowWidthSizeClass,\n    rcmdGridState: LazyGridState,\n    popularGridState: LazyGridState,\n    onChangeTabIndex: (Int) -> Unit,\n    popularViewModel: PopularViewModel = koinViewModel(),\n    recommendViewModel: RecommendViewModel = koinViewModel(),\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n\n    Column(\n        modifier = modifier\n            .background(MaterialTheme.colorScheme.surfaceContainer),\n    ) {\n        TabRow(\n            modifier = Modifier\n                .zIndex(1f),\n            selectedTabIndex = selectedTabIndex,\n            containerColor = MaterialTheme.colorScheme.surfaceContainer\n        ) {\n            HomeTab.entries.forEachIndexed { index, tab ->\n                Tab(\n                    selected = selectedTabIndex == index,\n                    onClick = {\n                        onChangeTabIndex(index)\n                    },\n                    text = {\n                        Text(\n                            text = tab.getDisplayName(context),\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                )\n            }\n        }\n\n        Surface(\n            color = MaterialTheme.colorScheme.surface,\n            shape = if (windowSize == WindowWidthSizeClass.Compact) RoundedCornerShape(0.dp) else MaterialTheme.shapes.medium,\n        ) {\n            HorizontalPager(\n                modifier = Modifier,\n                state = pageState,\n            ) { page ->\n                when (page) {\n                    0 -> {\n                        RcmdPage(\n                            state = rcmdGridState,\n                            windowSize = windowSize,\n                            videos = recommendViewModel.recommendVideoList,\n                            onClickVideo = { aid ->\n                                VideoPlayerActivity.actionStart(context = context, aid = aid)\n                            },\n                            loading = recommendViewModel.loading,\n                            refreshing = recommendViewModel.refreshing,\n                            onRefresh = {\n                                scope.launch(Dispatchers.IO) {\n                                    recommendViewModel.resetPage()\n                                    //避免刷新太快\n                                    delay(300)\n                                    recommendViewModel.loadMore {\n                                        //clear data before set new data\n                                        recommendViewModel.clearData()\n                                    }\n                                    recommendViewModel.refreshing = false\n                                }\n                            },\n                            loadMore = {\n                                scope.launch(Dispatchers.IO) {\n                                    recommendViewModel.loadMore()\n                                    recommendViewModel.refreshing = false\n                                }\n                            }\n                        )\n                    }\n\n                    1 -> {\n                        PopularPage(\n                            state = popularGridState,\n                            windowSize = windowSize,\n                            videos = popularViewModel.popularVideoList,\n                            onClickVideo = { aid ->\n                                VideoPlayerActivity.actionStart(context = context, aid = aid)\n                            },\n                            loading = popularViewModel.loading,\n                            refreshing = popularViewModel.refreshing,\n                            onRefresh = {\n                                scope.launch(Dispatchers.IO) {\n                                    popularViewModel.resetPage()\n                                    //避免刷新太快\n                                    delay(300)\n                                    popularViewModel.loadMore {\n                                        //clear data before set new data\n                                        popularViewModel.clearData()\n                                    }\n                                    popularViewModel.refreshing = false\n                                }\n                            },\n                            loadMore = {\n                                scope.launch(Dispatchers.IO) {\n                                    popularViewModel.loadMore()\n                                    popularViewModel.refreshing = false\n                                }\n                            }\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun HomeTopAppBar(\n    modifier: Modifier = Modifier,\n    windowSize: WindowWidthSizeClass,\n    avatar: String,\n    onShowUserDialog: () -> Unit,\n) {\n    if (windowSize == WindowWidthSizeClass.Compact) {\n        TopAppBar(\n            modifier = modifier,\n            title = {\n            },\n            navigationIcon = {},\n            actions = {\n                IconButton(onClick = onShowUserDialog) {\n                    if (avatar.isBlank()) {\n                        Icon(\n                            imageVector = Icons.Rounded.Person,\n                            contentDescription = null\n                        )\n                    } else {\n                        Box(\n                            modifier = Modifier\n                                .clip(CircleShape)\n                                .background(Color.Gray)\n                        ) {\n                            AsyncImage(\n                                modifier = Modifier\n                                    .size(36.dp),\n                                model = avatar,\n                                contentDescription = null,\n                                contentScale = ContentScale.Crop\n                            )\n                        }\n                    }\n                }\n            },\n            colors = TopAppBarDefaults.topAppBarColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainer,\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/SearchScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.home\n\nimport android.app.Activity\nimport android.content.res.Configuration\nimport androidx.compose.animation.ExperimentalSharedTransitionApi\nimport androidx.compose.animation.SharedTransitionLayout\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.text.input.rememberTextFieldState\nimport androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.AccessTime\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.DockedSearchBar\nimport androidx.compose.material3.ExpandedFullScreenSearchBar\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SearchBarDefaults\nimport androidx.compose.material3.SearchBarValue\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.rememberSearchBarState\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport dev.aaa1115910.biliapi.repositories.SearchTypeResult\nimport dev.aaa1115910.bv.mobile.activities.VideoPlayerActivity\nimport dev.aaa1115910.bv.mobile.component.preferences.items.listItemPreference\nimport dev.aaa1115910.bv.mobile.component.preferences.preferenceGroups\nimport dev.aaa1115910.bv.mobile.screen.home.search.SearchInputContent\nimport dev.aaa1115910.bv.mobile.screen.home.search.SearchResultContent\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.viewmodel.search.SearchInputViewModel\nimport dev.aaa1115910.bv.viewmodel.search.SearchResultViewModel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Composable\nfun SearchScreen(\n    modifier: Modifier = Modifier,\n    searchInputViewModel: SearchInputViewModel = koinViewModel(),\n    searchResultViewModel: SearchResultViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val windowSizeClass = calculateWindowSizeClass(context as Activity)\n    val windowSize = windowSizeClass.widthSizeClass\n\n    val updateKeyword: (String) -> Unit = { newKeyword ->\n        if (newKeyword != searchInputViewModel.keyword) {\n            searchInputViewModel.keyword = newKeyword\n            searchInputViewModel.updateSuggests()\n        }\n    }\n\n    val onSearch: (String) -> Unit = {\n        searchResultViewModel.keyword = it\n        searchResultViewModel.update()\n        searchInputViewModel.addSearchHistory(it)\n    }\n\n    val onOpenUgc: (Long) -> Unit = { aid ->\n        VideoPlayerActivity.actionStart(context = context, aid = aid)\n    }\n\n    SearchContent(\n        modifier = modifier,\n        windowSize = windowSize,\n        keywordSuggestions = searchInputViewModel.suggests,\n        historyKeywords = searchInputViewModel.searchHistories.map { it.keyword },\n        matchedHistory = searchInputViewModel.matchedSearchHistories.map { it.keyword },\n        updateKeyword = updateKeyword,\n        onSearch = onSearch,\n        onOpenUgc = onOpenUgc,\n        videoSearchResult = searchResultViewModel.videoSearchResult.videos,\n        mediaBangumiSearchResult = searchResultViewModel.mediaBangumiSearchResult.mediaBangumis,\n        mediaFtSearchResult = searchResultViewModel.mediaFtSearchResult.mediaFts,\n        biliUserSearchResult = searchResultViewModel.biliUserSearchResult.biliUsers,\n        liveRoomSearchResult = searchResultViewModel.liveRoomSearchResult.liveRooms\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)\n@Composable\nfun SearchContent(\n    modifier: Modifier = Modifier,\n    windowSize: WindowWidthSizeClass,\n    keywordSuggestions: List<String> = emptyList(),\n    historyKeywords: List<String>,\n    matchedHistory: List<String>,\n    updateKeyword: (String) -> Unit = {},\n    onSearch: (String) -> Unit = {},\n    onOpenUgc: (Long) -> Unit = {},\n    videoSearchResult: List<SearchTypeResult.Video>,\n    mediaBangumiSearchResult: List<SearchTypeResult.Pgc>,\n    mediaFtSearchResult: List<SearchTypeResult.Pgc>,\n    biliUserSearchResult: List<SearchTypeResult.User>,\n    liveRoomSearchResult: List<SearchTypeResult.LiveRoom>\n) {\n    val scope = rememberCoroutineScope()\n    val searchBarState = rememberSearchBarState()\n    val textFieldState = rememberTextFieldState()\n    val navController = rememberNavController()\n\n    var searchBarExpanded by remember { mutableStateOf(false) }\n    var textFieldFocused by remember { mutableStateOf(false) }\n\n    LaunchedEffect(textFieldState.text, textFieldFocused) {\n        println(\"Text field state: $textFieldState\")\n        searchBarExpanded = textFieldState.text != \"\" && textFieldFocused\n        updateKeyword(textFieldState.text.toString())\n    }\n\n    val onSearchKeyword: (String) -> Unit = {\n        onSearch(it)\n        if (navController.currentDestination?.route != \"searchResult\") navController.navigate(\"searchResult\")\n        textFieldState.setTextAndPlaceCursorAtEnd(it)\n        scope.launch {\n            // 等到 searchBar 移动到顶部再收起\n            delay(500)\n            searchBarState.animateToCollapsed()\n        }\n    }\n\n    val inputField = @Composable {\n        SearchBarDefaults.InputField(\n            modifier = Modifier.onFocusChanged { textFieldFocused = it.isFocused },\n            searchBarState = searchBarState,\n            textFieldState = textFieldState,\n            onSearch = onSearchKeyword,\n            placeholder = { Text(text = \"在此处输入文字\") },\n        )\n    }\n\n    SharedTransitionLayout(\n        modifier = modifier\n    ) {\n        NavHost(\n            navController = navController,\n            startDestination = \"searchInput\"\n        ) {\n            composable(\"searchInput\") {\n                SearchInputContent(\n                    windowSize = windowSize,\n                    keywordSuggestions = keywordSuggestions,\n                    keywordHistories = historyKeywords,\n                    matchedKeyworkHistories = matchedHistory,\n                    searchBarState = searchBarState,\n                    textFieldState = textFieldState,\n                    searchBarExpanded = searchBarExpanded,\n                    onSearchBarExpandedChange = { searchBarExpanded = it },\n                    sharedTransitionScope = this@SharedTransitionLayout,\n                    animatedVisibilityScope = this@composable,\n                    inputField = inputField,\n                    onSearch = onSearchKeyword\n                )\n            }\n            composable(\"searchResult\") {\n                SearchResultContent(\n                    modifier = Modifier.fillMaxSize(),\n                    searchBarState = searchBarState,\n                    textFieldState = textFieldState,\n                    keywordSuggestions = keywordSuggestions,\n                    historyKeywords = historyKeywords,\n                    matchedHistory = matchedHistory,\n                    sharedTransitionScope = this@SharedTransitionLayout,\n                    animatedVisibilityScope = this@composable,\n                    inputField = inputField,\n                    videoSearchResult = videoSearchResult,\n                    mediaBangumiSearchResult = mediaBangumiSearchResult,\n                    mediaFtSearchResult = mediaFtSearchResult,\n                    biliUserSearchResult = biliUserSearchResult,\n                    liveRoomSearchResult = liveRoomSearchResult,\n                    onSearch = onSearchKeyword,\n                    onOpenUgc = onOpenUgc\n                )\n            }\n        }\n    }\n\n    if (windowSize == WindowWidthSizeClass.Compact) {\n        ExpandedFullScreenSearchBar(\n            state = searchBarState,\n            inputField = inputField\n        ) {\n            SearchBarResultContent(\n                modifier = Modifier.fillMaxSize(),\n                keyword = textFieldState.text.toString(),\n                recentHistory = historyKeywords,\n                matchedHistory = matchedHistory,\n                suggestions = keywordSuggestions,\n                onSearch = onSearchKeyword,\n                onDeleteHistory = {}\n            )\n        }\n    }\n}\n\n\n@Composable\nfun SearchBarResultContent(\n    modifier: Modifier = Modifier,\n    keyword: String,\n    recentHistory: List<String>,\n    matchedHistory: List<String>,\n    suggestions: List<String>,\n    onSearch: (String) -> Unit,\n    onDeleteHistory: (String) -> Unit\n) {\n    val listItemColors = ListItemDefaults.colors().copy(\n        containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,\n    )\n\n    Surface(\n        modifier = modifier,\n        color = MaterialTheme.colorScheme.surfaceContainer\n    ) {\n        LazyColumn(\n            modifier = Modifier,\n            contentPadding = PaddingValues(12.dp)\n        ) {\n            preferenceGroups(\n                \"历史记录\" to {\n                    if (keyword.isNotEmpty()) {\n                        matchedHistory.take(10).map {\n                            listItemPreference(\n                                headlineContent = { Text(text = it) },\n                                leadingContent = {\n                                    Box(\n                                        modifier = Modifier\n                                            .clip(CircleShape)\n                                            .background(MaterialTheme.colorScheme.surface),\n                                    ) {\n                                        Icon(\n                                            modifier = Modifier.padding(3.dp),\n                                            imageVector = Icons.Default.AccessTime,\n                                            contentDescription = \"search history icon\",\n                                        )\n                                    }\n                                },\n                                colors = listItemColors,\n                                onClick = { onSearch(it) }\n                            )\n                        }\n                    } else {\n                        recentHistory.take(10).map {\n                            listItemPreference(\n                                headlineContent = { Text(text = it) },\n                                leadingContent = {\n                                    Box(\n                                        modifier = Modifier\n                                            .clip(CircleShape)\n                                            .background(MaterialTheme.colorScheme.surface),\n                                    ) {\n                                        Icon(\n                                            modifier = Modifier.padding(3.dp),\n                                            imageVector = Icons.Default.AccessTime,\n                                            contentDescription = \"search history icon\",\n                                        )\n                                    }\n                                },\n                                colors = listItemColors,\n                                onClick = { onSearch(it) }\n                            )\n                        }\n                    }\n                },\n                \"搜索建议\" to {\n                    if (keyword.isNotEmpty()) {\n                        suggestions.map {\n                            listItemPreference(\n                                headlineContent = { Text(text = it) },\n                                leadingContent = {\n                                    Box(\n                                        modifier = Modifier\n                                            .clip(CircleShape)\n                                            .background(MaterialTheme.colorScheme.surface),\n                                    ) {\n                                        Icon(\n                                            modifier = Modifier.padding(3.dp),\n                                            imageVector = Icons.Default.Search,\n                                            contentDescription = \"search suggestion icon\",\n                                        )\n                                    }\n                                },\n                                colors = listItemColors,\n                                onClick = { onSearch(it) }\n                            )\n                        }\n                    }\n                }\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun SearchScreenMobilePreview() {\n    BVMobileTheme {\n        SearchContent(\n            windowSize = WindowWidthSizeClass.Compact,\n            videoSearchResult = emptyList(),\n            mediaBangumiSearchResult = emptyList(),\n            mediaFtSearchResult = emptyList(),\n            biliUserSearchResult = emptyList(),\n            liveRoomSearchResult = emptyList(),\n            historyKeywords = emptyList(),\n            matchedHistory = emptyList()\n        )\n    }\n}\n\n@Preview(device = \"spec:width=1280dp,height=800dp,dpi=240\")\n@Composable\nprivate fun SearchScreenTablePreview() {\n    BVMobileTheme {\n        SearchContent(\n            windowSize = WindowWidthSizeClass.Expanded,\n            videoSearchResult = emptyList(),\n            mediaBangumiSearchResult = emptyList(),\n            mediaFtSearchResult = emptyList(),\n            biliUserSearchResult = emptyList(),\n            liveRoomSearchResult = emptyList(),\n            historyKeywords = emptyList(),\n            matchedHistory = emptyList()\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun SearchBarResultCompatPreview() {\n    val inputField = @Composable {\n        SearchBarDefaults.InputField(\n            searchBarState = rememberSearchBarState(),\n            textFieldState = rememberTextFieldState(),\n            onSearch = {},\n            placeholder = { Text(text = \"在此处输入文字\") },\n        )\n    }\n\n    BVMobileTheme {\n        ExpandedFullScreenSearchBar(\n            state = rememberSearchBarState(\n                initialValue = SearchBarValue.Expanded\n            ),\n            inputField = inputField\n        ) {\n            SearchBarResultContent(\n                modifier = Modifier.fillMaxSize(),\n                keyword = \"123\",\n                recentHistory = listOf(\"123\", \"456\", \"789\"),\n                matchedHistory = listOf(\"123\", \"456\", \"789\"),\n                suggestions = listOf(\"123\", \"456\", \"789\"),\n                onSearch = {},\n                onDeleteHistory = {}\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun SearchBarResultDockedPreview() {\n    val inputField = @Composable {\n        SearchBarDefaults.InputField(\n            searchBarState = rememberSearchBarState(),\n            textFieldState = rememberTextFieldState(),\n            onSearch = {},\n            placeholder = { Text(text = \"在此处输入文字\") },\n        )\n    }\n\n    BVMobileTheme {\n        DockedSearchBar(\n            expanded = true,\n            onExpandedChange = {},\n            inputField = inputField,\n        ) {\n            SearchBarResultContent(\n                keyword = \"123\",\n                recentHistory = listOf(\"123\", \"456\", \"789\"),\n                matchedHistory = listOf(\"123\", \"456\", \"789\"),\n                suggestions = listOf(\"123\", \"456\", \"789\"),\n                onSearch = {},\n                onDeleteHistory = {}\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/home/PopularPage.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.home.home\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material.pullrefresh.PullRefreshIndicator\nimport androidx.compose.material.pullrefresh.pullRefresh\nimport androidx.compose.material.pullrefresh.rememberPullRefreshState\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.mobile.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.util.OnBottomReached\nimport dev.aaa1115910.bv.util.fInfo\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun PopularPage(\n    state: LazyGridState,\n    windowSize: WindowWidthSizeClass,\n    videos: List<UgcItem>,\n    onClickVideo: (aid: Long) -> Unit,\n    loading: Boolean,\n    refreshing: Boolean,\n    onRefresh: () -> Unit,\n    loadMore: () -> Unit\n) {\n    val logger = KotlinLogging.logger { }\n    val pullRefreshState = rememberPullRefreshState(refreshing, { onRefresh() })\n\n    state.OnBottomReached(\n        loading = loading\n    ) {\n        logger.fInfo { \"on reached popular page bottom\" }\n        loadMore()\n    }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .pullRefresh(state = pullRefreshState)\n    ) {\n        LazyVerticalGrid(\n            state = state,\n            columns = GridCells.Adaptive(if (windowSize == WindowWidthSizeClass.Compact) 180.dp else 220.dp),\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n            contentPadding = PaddingValues(8.dp)\n        ) {\n            items(videos) { video ->\n                SmallVideoCard(\n                    data = VideoCardData(\n                        avid = video.aid,\n                        title = video.title,\n                        cover = video.cover,\n                        play = video.play,\n                        danmaku = video.danmaku,\n                        upName = video.author,\n                        time = video.duration * 1000L\n                    ),\n                    onClick = { onClickVideo(video.aid) }\n                )\n            }\n        }\n        PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/home/RcmdPage.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.home.home\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material.pullrefresh.PullRefreshIndicator\nimport androidx.compose.material.pullrefresh.pullRefresh\nimport androidx.compose.material.pullrefresh.rememberPullRefreshState\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.mobile.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.util.OnBottomReached\nimport dev.aaa1115910.bv.util.fInfo\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun RcmdPage(\n    state: LazyGridState,\n    windowSize: WindowWidthSizeClass,\n    videos: List<UgcItem>,\n    onClickVideo: (aid: Long) -> Unit,\n    loading: Boolean,\n    refreshing: Boolean,\n    onRefresh: () -> Unit,\n    loadMore: () -> Unit\n) {\n    val logger = KotlinLogging.logger { }\n    val pullRefreshState = rememberPullRefreshState(refreshing, { onRefresh() })\n\n    state.OnBottomReached(\n        loading = loading\n    ) {\n        logger.fInfo { \"on reached rcmd page bottom\" }\n        loadMore()\n    }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .pullRefresh(state = pullRefreshState)\n    ) {\n        LazyVerticalGrid(\n            state = state,\n            columns = GridCells.Adaptive(if (windowSize == WindowWidthSizeClass.Compact) 180.dp else 220.dp),\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n            contentPadding = PaddingValues(8.dp)\n        ) {\n            items(videos) { video ->\n                SmallVideoCard(\n                    data = VideoCardData(\n                        avid = video.aid,\n                        title = video.title,\n                        cover = video.cover,\n                        play = video.play,\n                        danmaku = video.danmaku,\n                        upName = video.author,\n                        time = video.duration * 1000L\n                    ),\n                    onClick = { onClickVideo(video.aid) }\n                )\n            }\n        }\n        PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/search/SearchInput.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.home.search\n\nimport androidx.compose.animation.AnimatedVisibilityScope\nimport androidx.compose.animation.ExperimentalSharedTransitionApi\nimport androidx.compose.animation.SharedTransitionScope\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.input.TextFieldState\nimport androidx.compose.foundation.text.input.rememberTextFieldState\nimport androidx.compose.material3.DockedSearchBar\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SearchBar\nimport androidx.compose.material3.SearchBarState\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberSearchBarState\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.mobile.screen.home.SearchBarResultContent\n\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)\n@Composable\nfun SearchInputContent(\n    modifier: Modifier = Modifier,\n    windowSize: WindowWidthSizeClass,\n    keywordSuggestions: List<String> = emptyList(),\n    keywordHistories: List<String> = emptyList(),\n    matchedKeyworkHistories: List<String> = emptyList(),\n    searchBarState: SearchBarState = rememberSearchBarState(),\n    textFieldState: TextFieldState = rememberTextFieldState(),\n    searchBarExpanded: Boolean,\n    onSearchBarExpandedChange: (Boolean) -> Unit,\n    sharedTransitionScope: SharedTransitionScope,\n    animatedVisibilityScope: AnimatedVisibilityScope,\n    inputField: @Composable () -> Unit = {},\n    onSearch: (String) -> Unit\n) {\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            when (windowSize) {\n                WindowWidthSizeClass.Compact -> {\n\n                }\n\n                else -> {\n                    TopAppBar(\n                        title = {},\n                        colors = TopAppBarDefaults.topAppBarColors(\n                            containerColor = MaterialTheme.colorScheme.surfaceContainer,\n                        )\n                    )\n                }\n            }\n        },\n        containerColor = MaterialTheme.colorScheme.surfaceContainer\n    ) { innerPadding ->\n        Surface(\n            modifier = Modifier\n                .padding(top = innerPadding.calculateTopPadding())\n                .fillMaxSize(),\n            color = MaterialTheme.colorScheme.surface,\n            shape = if (windowSize == WindowWidthSizeClass.Compact) RoundedCornerShape(0.dp) else MaterialTheme.shapes.large,\n        ) {\n            Box(\n                modifier = Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center\n            ) {\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    verticalArrangement = Arrangement.spacedBy(32.dp)\n                ) {\n                    Text(\n                        text = \"搜索\",\n                        style = MaterialTheme.typography.displaySmall\n                    )\n\n                    when (windowSize) {\n                        WindowWidthSizeClass.Compact -> {\n                            with(sharedTransitionScope) {\n                                SearchBar(\n                                    modifier = Modifier\n                                        .sharedElement(\n                                            sharedContentState = rememberSharedContentState(\"searchBar\"),\n                                            animatedVisibilityScope = animatedVisibilityScope\n                                        ),\n                                    state = searchBarState,\n                                    inputField = inputField\n                                )\n                            }\n                        }\n\n                        else -> {\n                            with(sharedTransitionScope) {\n                                DockedSearchBar(\n                                    modifier = Modifier\n                                        .imePadding()\n                                        .sharedElement(\n                                            sharedContentState = rememberSharedContentState(\"dockedSearchBar\"),\n                                            animatedVisibilityScope = animatedVisibilityScope\n                                        ),\n                                    inputField = inputField,\n                                    expanded = searchBarExpanded,\n                                    onExpandedChange = onSearchBarExpandedChange,\n                                ) {\n                                    SearchBarResultContent(\n                                        keyword = textFieldState.text.toString(),\n                                        recentHistory = keywordHistories,\n                                        matchedHistory = matchedKeyworkHistories,\n                                        suggestions = keywordSuggestions,\n                                        onSearch = onSearch,\n                                        onDeleteHistory = {}\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/home/search/SearchResult.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.home.search\n\nimport android.app.Activity\nimport androidx.compose.animation.AnimatedVisibilityScope\nimport androidx.compose.animation.ExperimentalSharedTransitionApi\nimport androidx.compose.animation.SharedTransitionScope\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.statusBarsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.input.TextFieldState\nimport androidx.compose.foundation.text.input.rememberTextFieldState\nimport androidx.compose.material3.ExpandedDockedSearchBar\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.PrimaryScrollableTabRow\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SearchBar\nimport androidx.compose.material3.SearchBarState\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Tab\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.rememberSearchBarState\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowWidthSizeClass\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.ugc.toSmartDate\nimport dev.aaa1115910.biliapi.repositories.SearchType\nimport dev.aaa1115910.biliapi.repositories.SearchTypeResult\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.mobile.component.search.UgcListItem\nimport dev.aaa1115910.bv.mobile.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.mobile.screen.home.SearchBarResultContent\nimport dev.aaa1115910.bv.util.removeHtmlTags\n\n\n@OptIn(\n    ExperimentalSharedTransitionApi::class,\n    ExperimentalMaterial3Api::class,\n    ExperimentalMaterial3WindowSizeClassApi::class\n)\n@Composable\nfun SearchResultContent(\n    modifier: Modifier = Modifier,\n    searchBarState: SearchBarState = rememberSearchBarState(),\n    textFieldState: TextFieldState = rememberTextFieldState(),\n    keywordSuggestions: List<String>,\n    historyKeywords: List<String>,\n    matchedHistory: List<String>,\n    sharedTransitionScope: SharedTransitionScope,\n    animatedVisibilityScope: AnimatedVisibilityScope,\n    inputField: @Composable () -> Unit = {},\n    videoSearchResult: List<SearchTypeResult.Video>,\n    mediaBangumiSearchResult: List<SearchTypeResult.Pgc>,\n    mediaFtSearchResult: List<SearchTypeResult.Pgc>,\n    biliUserSearchResult: List<SearchTypeResult.User>,\n    liveRoomSearchResult: List<SearchTypeResult.LiveRoom>,\n    onSearch: (String) -> Unit,\n    onOpenUgc: (Long) -> Unit\n) {\n    val context = LocalContext.current\n    val windowSize = calculateWindowSizeClass(context as Activity).widthSizeClass\n    var searchType by remember { mutableStateOf(SearchType.Video) }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            when (windowSize) {\n                WindowWidthSizeClass.Compact -> {\n                    Column(\n                        modifier = Modifier\n                            .statusBarsPadding()\n                    ) {\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            horizontalArrangement = Arrangement.Center\n                        ) {\n                            with(sharedTransitionScope) {\n                                SearchBar(\n                                    modifier = Modifier\n                                        .padding(vertical = 4.dp)\n                                        .sharedElement(\n                                            sharedContentState = rememberSharedContentState(\"searchBar\"),\n                                            animatedVisibilityScope = animatedVisibilityScope\n                                        ),\n                                    state = searchBarState,\n                                    inputField = inputField\n                                )\n                            }\n                        }\n                        PrimaryScrollableTabRow(\n                            selectedTabIndex = searchType.ordinal,\n                        ) {\n                            SearchType.entries.forEachIndexed { index, title ->\n                                Tab(\n                                    selected = searchType.ordinal == index,\n                                    onClick = { searchType = title },\n                                    text = { Text(text = title.name) },\n                                )\n                            }\n                        }\n                    }\n                }\n\n                else -> {\n                    Row(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .background(MaterialTheme.colorScheme.surfaceContainer)\n                            .statusBarsPadding()\n                    ) {\n                        with(sharedTransitionScope) {\n                            Box(\n                                modifier = Modifier\n                                    .padding(vertical = 4.dp, horizontal = 36.dp)\n                                    .sharedElement(\n                                        sharedContentState = rememberSharedContentState(\"dockedSearchBar\"),\n                                        animatedVisibilityScope = animatedVisibilityScope\n                                    )\n                            ) {\n                                SearchBar(\n                                    modifier = Modifier,\n                                    state = searchBarState,\n                                    inputField = inputField\n                                )\n                                ExpandedDockedSearchBar(\n                                    state = searchBarState,\n                                    inputField = inputField\n                                ) {\n                                    SearchBarResultContent(\n                                        keyword = textFieldState.text.toString(),\n                                        recentHistory = historyKeywords,\n                                        matchedHistory = matchedHistory,\n                                        suggestions = keywordSuggestions,\n                                        onSearch = onSearch,\n                                        onDeleteHistory = {}\n                                    )\n                                }\n                            }\n                        }\n\n                        PrimaryScrollableTabRow(\n                            modifier = Modifier\n                                .align(Alignment.Bottom),\n                            selectedTabIndex = searchType.ordinal,\n                            containerColor = MaterialTheme.colorScheme.surfaceContainer\n                        ) {\n                            SearchType.entries.forEachIndexed { index, title ->\n                                Tab(\n                                    selected = searchType.ordinal == index,\n                                    onClick = { searchType = title },\n                                    text = { Text(text = title.name) },\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        },\n        containerColor = MaterialTheme.colorScheme.surfaceContainer\n    ) { innerPadding ->\n        Surface(\n            modifier = Modifier\n                .padding(top = innerPadding.calculateTopPadding())\n                .fillMaxSize(),\n            color = MaterialTheme.colorScheme.surface,\n            shape = if (windowSize == WindowWidthSizeClass.Compact) RoundedCornerShape(0.dp) else MaterialTheme.shapes.large,\n        ) {\n            when (searchType) {\n                SearchType.Video -> VideoSearchResult(\n                    videoList = videoSearchResult,\n                    onClickVideo = onOpenUgc\n                )\n\n                SearchType.MediaBangumi -> MediaBangumiSearchResult(\n                    mediaBangumiList = mediaBangumiSearchResult\n                )\n\n                SearchType.MediaFt -> MediaFtSearchResult(\n                    mediaFtList = mediaFtSearchResult\n                )\n\n                SearchType.BiliUser -> BiliUserSearchResult(\n                    biliUserList = biliUserSearchResult\n                )\n\n                SearchType.LiveRoom -> LiveRoomSearchResult(\n                    biliUserList = liveRoomSearchResult\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)\n@Composable\nprivate fun VideoSearchResult(\n    modifier: Modifier = Modifier,\n    videoList: List<SearchTypeResult.Video>,\n    onClickVideo: (aid: Long) -> Unit\n) {\n    val context = LocalContext.current\n    val windowSize = calculateWindowSizeClass(context as Activity).widthSizeClass\n\n    when (windowSize) {\n        WindowWidthSizeClass.Compact -> LazyColumn(\n            modifier = modifier\n        ) {\n            items(videoList) { video ->\n                UgcListItem(\n                    data = VideoCardData(\n                        avid = video.aid,\n                        title = video.title.removeHtmlTags(),\n                        cover = video.cover,\n                        play = video.play,\n                        danmaku = video.danmaku,\n                        upName = video.author,\n                        time = video.duration * 1000L,\n                        pubTime = video.pubDate.toLong().toSmartDate()\n                    ),\n                    onClick = { onClickVideo(video.aid) }\n                )\n            }\n        }\n\n        else -> LazyVerticalGrid(\n            modifier = modifier,\n            columns = GridCells.Adaptive(220.dp),\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n            contentPadding = PaddingValues(8.dp)\n        ) {\n            items(videoList) { video ->\n                SmallVideoCard(\n                    data = VideoCardData(\n                        avid = video.aid,\n                        title = video.title.removeHtmlTags(),\n                        cover = video.cover,\n                        play = video.play,\n                        danmaku = video.danmaku,\n                        upName = video.author,\n                        time = video.duration * 1000L,\n                        pubTime = video.pubDate.toLong().toSmartDate()\n                    ),\n                    onClick = { onClickVideo(video.aid) }\n                )\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun MediaBangumiSearchResult(\n    modifier: Modifier = Modifier,\n    mediaBangumiList: List<SearchTypeResult.Pgc>,\n) {\n\n}\n\n\n@Composable\nprivate fun MediaFtSearchResult(\n    modifier: Modifier = Modifier,\n    mediaFtList: List<SearchTypeResult.Pgc>,\n) {\n\n}\n\n\n@Composable\nprivate fun BiliUserSearchResult(\n    modifier: Modifier = Modifier,\n    biliUserList: List<SearchTypeResult.User>,\n) {\n\n}\n\n@Composable\nprivate fun LiveRoomSearchResult(\n    modifier: Modifier = Modifier,\n    biliUserList: List<SearchTypeResult.LiveRoom>,\n) {\n\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/SettingsCategories.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.settings\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.mobile.component.preferences.items.textPreference\nimport dev.aaa1115910.bv.mobile.component.preferences.preferenceGroups\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SettingsCategories(\n    modifier: Modifier = Modifier,\n    selectedSettings: MobileSettings?,\n    onSelectedSettings: (MobileSettings) -> Unit,\n    showNavBack: Boolean,\n    onBack: () -> Unit\n) {\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            LargeTopAppBar(\n                title = { Text(text = \"Settings\") },\n                navigationIcon = {\n                    IconButton(onClick = onBack) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                            contentDescription = null\n                        )\n                    }.takeIf { showNavBack }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n                )\n            )\n        },\n        containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier.padding(innerPadding),\n            contentPadding = PaddingValues(horizontal = 18.dp)\n        ) {\n            preferenceGroups(\n                null to {\n                    listOf(\n                        MobileSettings.Play,\n                        MobileSettings.Advance\n                    ).forEach { item ->\n                        textPreference(\n                            title = item.title,\n                            summary = item.summary,\n                            onClick = { onSelectedSettings(item) },\n                            selected = selectedSettings == item\n                        )\n                    }\n                },\n                null to {\n                    textPreference(\n                        title = MobileSettings.About.title,\n                        summary = MobileSettings.About.summary,\n                        onClick = { onSelectedSettings(MobileSettings.About) },\n                        selected = selectedSettings == MobileSettings.About\n                    )\n                },\n                null to {\n                    textPreference(\n                        title = MobileSettings.Debug.title,\n                        summary = MobileSettings.Debug.summary,\n                        onClick = { onSelectedSettings(MobileSettings.Debug) },\n                        selected = selectedSettings == MobileSettings.Debug\n                    )\n                }\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun SettingsCategoriesPreview() {\n    BVMobileTheme {\n        Surface {\n            SettingsCategories(\n                selectedSettings = MobileSettings.Play,\n                onSelectedSettings = {},\n                showNavBack = false,\n                onBack = {},\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/SettingsDetails.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.settings\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport dev.aaa1115910.bv.mobile.screen.settings.details.AboutContent\nimport dev.aaa1115910.bv.mobile.screen.settings.details.AdvanceContent\nimport dev.aaa1115910.bv.mobile.screen.settings.details.DebugContent\nimport dev.aaa1115910.bv.mobile.screen.settings.details.PlayContent\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SettingsDetails(\n    modifier: Modifier = Modifier,\n    selectedSettings: MobileSettings?,\n    showNavBack: Boolean,\n    onBack: () -> Unit\n) {\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            LargeTopAppBar(\n                title = {\n                    Text(text = selectedSettings?.title ?: \"NaN\")\n                },\n                navigationIcon = {\n                    if (showNavBack) {\n                        IconButton(onClick = onBack) {\n                            Icon(\n                                imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                                contentDescription = null\n                            )\n                        }\n                    }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n                )\n            )\n        },\n        containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n    ) { innerPadding ->\n        Box(modifier = Modifier.padding(innerPadding))\n        val contentModifier = Modifier.padding(top = innerPadding.calculateTopPadding())\n        when (selectedSettings) {\n            null, MobileSettings.Play -> PlayContent(modifier = contentModifier)\n            MobileSettings.About -> AboutContent(modifier = contentModifier)\n            MobileSettings.Debug -> DebugContent(modifier = contentModifier)\n            MobileSettings.Advance -> AdvanceContent(modifier = contentModifier)\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/SettingsScreen.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.settings\n\nimport android.app.Activity\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.material3.LocalMinimumInteractiveComponentSize\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.VerticalDragHandle\nimport androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi\nimport androidx.compose.material3.adaptive.currentWindowAdaptiveInfo\nimport androidx.compose.material3.adaptive.layout.AnimatedPane\nimport androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold\nimport androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole\nimport androidx.compose.material3.adaptive.layout.PaneExpansionAnchor\nimport androidx.compose.material3.adaptive.layout.PaneExpansionState\nimport androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope\nimport androidx.compose.material3.adaptive.layout.rememberPaneExpansionState\nimport androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport androidx.window.core.layout.WindowWidthSizeClass\nimport kotlinx.coroutines.launch\n\n@OptIn(ExperimentalMaterial3AdaptiveApi::class)\n@Composable\nfun SettingsScreen() {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator()\n\n    var selectedSettings by rememberSaveable { mutableStateOf<MobileSettings?>(null) }\n    val singlePart = listOf(WindowWidthSizeClass.COMPACT, WindowWidthSizeClass.MEDIUM)\n        .contains(currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass)\n\n    BackHandler(scaffoldNavigator.canNavigateBack()) {\n        scope.launch { scaffoldNavigator.navigateBack() }\n    }\n\n    ListDetailPaneScaffold(\n        modifier = Modifier\n            .background(MaterialTheme.colorScheme.surfaceContainerLow),\n        directive = scaffoldNavigator.scaffoldDirective,\n        value = scaffoldNavigator.scaffoldValue,\n        listPane = {\n            AnimatedPane(\n                modifier = Modifier.preferredWidth(360.dp),\n                enterTransition = fadeIn() + slideInHorizontally(),\n                exitTransition = fadeOut() + slideOutHorizontally()\n            ) {\n                SettingsCategories(\n                    selectedSettings = if (singlePart) null else selectedSettings\n                        ?: MobileSettings.Play,\n                    onSelectedSettings = {\n                        selectedSettings = it\n                        scope.launch {\n                            scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)\n                        }\n                    },\n                    showNavBack = !scaffoldNavigator.canNavigateBack(),\n                    onBack = { (context as Activity).finish() },\n                )\n            }\n        },\n        detailPane = {\n            AnimatedPane(\n                modifier = Modifier,\n                enterTransition = fadeIn() + slideInHorizontally { it / 2 },\n                exitTransition = fadeOut() + slideOutHorizontally { it / 2 }\n            ) {\n                SettingsDetails(\n                    selectedSettings = selectedSettings ?: MobileSettings.Play,\n                    showNavBack = scaffoldNavigator.canNavigateBack(),\n                    onBack = { scope.launch { scaffoldNavigator.navigateBack() } }\n                )\n            }\n        },\n        paneExpansionDragHandle = { state -> PaneExpansionDragHandle(state) },\n        paneExpansionState = rememberPaneExpansionState(\n            keyProvider = scaffoldNavigator.scaffoldValue,\n            anchors = PaneExpansionAnchors,\n        )\n    )\n}\n\n@OptIn(ExperimentalMaterial3AdaptiveApi::class)\n@Composable\nfun ThreePaneScaffoldScope.PaneExpansionDragHandle(\n    state: PaneExpansionState = rememberPaneExpansionState()\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n    VerticalDragHandle(\n        modifier = Modifier\n            .paneExpansionDraggable(\n                state,\n                LocalMinimumInteractiveComponentSize.current,\n                interactionSource,\n            ),\n        interactionSource = interactionSource\n    )\n}\n\nenum class MobileSettings(\n    val title: String,\n    val summary: String? = null\n) {\n    Play(title = \"播放设置\", summary = \"画质编码、音频、循环模式\"),\n    About(title = \"关于\", summary = \"一般不会有人点\"),\n    Advance(title = \"更多设置\", summary = \"接口\"),\n    Debug(title = \"调试\", \"瞅啥瞅\");\n}\n\nprivate val PaneExpansionAnchors = listOf(\n    PaneExpansionAnchor.Offset.fromStart(360.dp),\n    PaneExpansionAnchor.Proportion(0.5f),\n    PaneExpansionAnchor.Offset.fromEnd(360.dp),\n)\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/details/AboutContent.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.settings.details\n\nimport android.content.Intent\nimport android.content.res.Configuration\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.mobile.component.preferences.items.textPreference\nimport dev.aaa1115910.bv.mobile.component.preferences.preferenceGroup\nimport dev.aaa1115910.bv.mobile.component.settings.UpdateDialog\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@Composable\nfun AboutContent(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n    var showUpdateDialog by remember { mutableStateOf(false) }\n\n    LazyColumn(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally,\n        contentPadding = PaddingValues(horizontal = 18.dp)\n    ) {\n        item {\n            AppIcon(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(vertical = 24.dp)\n            )\n        }\n\n        preferenceGroup {\n            textPreference(\n                title = \"当前版本\",\n                summary = \"${BuildConfig.VERSION_NAME}.${BuildConfig.BUILD_TYPE}\",\n                onClick = { showUpdateDialog = true }\n            )\n            textPreference(\n                title = \"项目地址\",\n                summary = \"https://github.com/aaa1115910/bv\",\n                onClick = {\n                    val url = \"https://github.com/aaa1115910/bv\"\n                    val intent = Intent(Intent.ACTION_VIEW, url.toUri())\n                    context.startActivity(intent)\n                }\n            )\n        }\n    }\n\n    UpdateDialog(\n        show = showUpdateDialog,\n        onHideDialog = { showUpdateDialog = false }\n    )\n}\n\n@Composable\nprivate fun AppIcon(modifier: Modifier = Modifier) {\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Image(\n            modifier = Modifier\n                .width(256.dp)\n                .height(128.dp),\n            painter = painterResource(id = dev.aaa1115910.bv.R.drawable.ic_launcher_foreground),\n            contentDescription = null,\n            contentScale = ContentScale.FillWidth\n        )\n        Text(\n            text = \"Bug Video\",\n            style = MaterialTheme.typography.titleLarge,\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun AppIconPreview() {\n    BVMobileTheme {\n        Surface {\n            AppIcon()\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Preview(device = \"spec:width=1280dp,height=800dp,dpi=240\")\n@Preview(\n    device = \"spec:width=1280dp,height=800dp,dpi=240\",\n    uiMode = Configuration.UI_MODE_NIGHT_YES\n)\n@Composable\nprivate fun AboutContentPreview() {\n    BVMobileTheme {\n        Surface(\n            color = MaterialTheme.colorScheme.surfaceContainerLow\n        ) {\n            AboutContent(\n                modifier = Modifier.fillMaxSize()\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/details/AdvanceContent.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.settings.details\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.activities.LauncherActivity\nimport dev.aaa1115910.bv.entity.InterfaceMode\nimport dev.aaa1115910.bv.mobile.component.preferences.items.radioPreference\nimport dev.aaa1115910.bv.mobile.component.preferences.preferenceGroups\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.util.PrefKeys\nimport dev.aaa1115910.bv.util.Prefs\n\n@Composable\nfun AdvanceContent(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n    val interfaceMode = Prefs.interfaceMode\n    val interfaceModeTitle = stringResource(R.string.settings_ui_interface_mode_title)\n    val apiTitle = stringResource(R.string.settings_item_api)\n\n    LazyColumn(\n        modifier = modifier,\n        contentPadding = PaddingValues(horizontal = 18.dp)\n    ) {\n        preferenceGroups(\n            null to {\n                radioPreference(\n                    title = interfaceModeTitle,\n                    value = interfaceMode,\n                    values = InterfaceMode.entries.associateWith { it.getDisplayName(context) },\n                    onValueChange = {\n                        if (it != interfaceMode) {\n                            Prefs.interfaceMode = it\n                            LauncherActivity.actionRestart(context)\n                        }\n                    }\n                )\n                radioPreference(\n                    title = apiTitle,\n                    prefReq = PrefKeys.prefApiTypeRequest,\n                    values = ApiType.entries.associate { it.ordinal to it.name }\n                        .toSortedMap { a, b -> a.compareTo(b) }\n                )\n            }\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun AdvanceContentPreview() {\n    BVMobileTheme {\n        Surface(\n            color = MaterialTheme.colorScheme.surfaceContainerLow\n        ) {\n            AdvanceContent(\n                modifier = Modifier.padding(8.dp)\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/details/DebugContent.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.settings.details\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\n\n@Composable\nfun DebugContent(\n    modifier: Modifier = Modifier\n) {\n\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun DebugContentPreview() {\n    BVMobileTheme {\n        Surface(\n            color = MaterialTheme.colorScheme.surfaceContainerLow\n        ) {\n            DebugContent(\n                modifier = Modifier.padding(8.dp)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/screen/settings/details/PlayContent.kt",
    "content": "package dev.aaa1115910.bv.mobile.screen.settings.details\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.mobile.component.preferences.items.radioPreference\nimport dev.aaa1115910.bv.mobile.component.preferences.preferenceGroups\nimport dev.aaa1115910.bv.mobile.theme.BVMobileTheme\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.util.PrefKeys\n\n@Composable\nfun PlayContent(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n\n    LazyColumn(\n        modifier = modifier,\n        contentPadding = PaddingValues(horizontal = 18.dp)\n    ) {\n        preferenceGroups(\n            \"画面\" to {\n                radioPreference(\n                    title = \"默认画质\",\n                    prefReq = PrefKeys.prefDefaultQualityRequest,\n                    values = Resolution.entries.associate { it.code to it.getDisplayName(context) }\n                        .toSortedMap { a, b -> a.compareTo(b) }\n                )\n                radioPreference(\n                    title = \"默认视频编码\",\n                    prefReq = PrefKeys.prefDefaultVideoCodecRequest,\n                    values = VideoCodec.entries.associate { it.codecId to it.getDisplayName(context) }\n                        .toSortedMap { a, b -> a.compareTo(b) }\n                )\n            },\n            \"音频\" to {\n                radioPreference(\n                    title = \"默认音频\",\n                    prefReq = PrefKeys.prefDefaultAudioRequest,\n                    values = Audio.entries.associate { it.code to it.getDisplayName(context) }\n                        .toSortedMap { a, b -> a.compareTo(b) }\n                )\n            }\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun PlayContentPreview() {\n    BVMobileTheme {\n        Surface(\n            color = MaterialTheme.colorScheme.surfaceContainerLow\n        ) {\n            PlayContent(\n                modifier = Modifier.padding(8.dp)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/mobile/src/main/kotlin/dev/aaa1115910/bv/mobile/theme/Theme.kt",
    "content": "package dev.aaa1115910.bv.mobile.theme\n\nimport android.app.Activity\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalView\nimport androidx.core.view.WindowCompat\nimport com.google.accompanist.systemuicontroller.rememberSystemUiController\n\n@Composable\nfun BVMobileTheme(\n    darkTheme: Boolean = isSystemInDarkTheme(),\n    dynamicColor: Boolean = true,\n    content: @Composable () -> Unit\n) {\n    val context = LocalContext.current\n    val window by lazy { (context as Activity).window }\n    val view = LocalView.current\n    val systemUiController = rememberSystemUiController()\n    val useDarkIcons = !isSystemInDarkTheme()\n\n    val colorScheme = when {\n        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {\n            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n        }\n\n        darkTheme -> darkColorScheme()\n        else -> lightColorScheme()\n    }\n\n    if (!view.isInEditMode) {\n        val currentWindow = (view.context as Activity).window\n        SideEffect {\n            (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()\n            WindowCompat.getInsetsController(currentWindow, view)\n                .isAppearanceLightStatusBars = darkTheme\n        }\n    }\n\n    SideEffect {\n        systemUiController.setStatusBarColor(color = Color.Transparent)\n        systemUiController.setNavigationBarColor(color = Color.Transparent)\n        if (!view.isInEditMode) {\n            WindowCompat.setDecorFitsSystemWindows(window, false)\n            WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =\n                useDarkIcons\n        }\n    }\n\n    MaterialTheme(\n        colorScheme = colorScheme,\n    ) {\n        content()\n    }\n}"
  },
  {
    "path": "app/mobile/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<resources>\n    <string name=\"qr_login_button_login\">二维码登录</string>\n    <string name=\"qr_token_result_button_add_user\">添加用户</string>\n    <string name=\"qr_token_result_toast_add_success\">添加成功</string>\n\n    <string name=\"title_mobile_activity_dynamic_detail\">动态详情</string>\n    <string name=\"title_mobile_activity_favorite\">我的收藏</string>\n    <string name=\"title_mobile_activity_following_season\">我的追番</string>\n    <string name=\"title_mobile_activity_following_user\">我的关注</string>\n    <string name=\"title_mobile_activity_history\">历史记录</string>\n    <string name=\"title_mobile_activity_intent_handler\">BV</string>\n    <string name=\"title_mobile_activity_login\">用户登录</string>\n    <string name=\"title_mobile_activity_qr_token_result\">扫码登录</string>\n    <string name=\"title_mobile_activity_settings\">设置</string>\n    <string name=\"title_mobile_activity_user_space\">用户空间</string>\n    <string name=\"title_mobile_activity_video_player\">视频播放</string>\n</resources>\n"
  },
  {
    "path": "app/mobile/src/main/res/values/themes.xml",
    "content": "<resources>\n\n    <style name=\"Theme.BV.Mobile.Splash\" parent=\"Theme.SplashScreen\">\n        <item name=\"postSplashScreenTheme\">@style/Theme.BV.Mobile</item>\n        <item name=\"windowSplashScreenAnimatedIcon\">@drawable/ic_launcher_foreground</item>\n    </style>\n\n    <style name=\"Theme.BV.Mobile\" parent=\"Theme.Material3.DayNight.NoActionBar\" />\n</resources>"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.kts.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n\n# okhttp\n-dontwarn org.bouncycastle.jsse.BCSSLParameters\n-dontwarn org.bouncycastle.jsse.BCSSLSocket\n-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider\n-dontwarn org.conscrypt.Conscrypt$Version\n-dontwarn org.conscrypt.Conscrypt\n-dontwarn org.conscrypt.ConscryptHostnameVerifier\n-dontwarn org.openjsse.javax.net.ssl.SSLParameters\n-dontwarn org.openjsse.javax.net.ssl.SSLSocket\n-dontwarn org.openjsse.net.ssl.OpenJSSE\n\n# kotlin serialization\n\n# Keep `Companion` object fields of serializable classes.\n# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.\n-if @kotlinx.serialization.Serializable class **\n-keepclassmembers class <1> {\n    static <1>$Companion Companion;\n}\n\n# Keep `serializer()` on companion objects (both default and named) of serializable classes.\n-if @kotlinx.serialization.Serializable class ** {\n    static **$* *;\n}\n-keepclassmembers class <2>$<3> {\n    kotlinx.serialization.KSerializer serializer(...);\n}\n\n# Keep `INSTANCE.serializer()` of serializable objects.\n-if @kotlinx.serialization.Serializable class ** {\n    public static ** INSTANCE;\n}\n-keepclassmembers class <1> {\n    public static <1> INSTANCE;\n    kotlinx.serialization.KSerializer serializer(...);\n}\n\n# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.\n-keepattributes RuntimeVisibleAnnotations,AnnotationDefault\n\n# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.\n# If you have any, uncomment and replace classes with those containing named companion objects.\n#-keepattributes InnerClasses # Needed for `getDeclaredClasses`.\n#-if @kotlinx.serialization.Serializable class\n#com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions.\n#com.example.myapplication.HasNamedCompanion2\n#{\n#    static **$* *;\n#}\n#-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.\n#    static <1>$$serializer INSTANCE;\n#}\n\n# ktor 混淆后，请求参数会莫名其妙消失\n-keep class io.ktor.**\n# 这部分是加上不混淆 ktor 后冒出来的 missing rules\n-dontwarn java.lang.management.ManagementFactory\n-dontwarn java.lang.management.RuntimeMXBean\n\n# LibVLC\n-keep class org.videolan.libvlc.** { *; }\n\n# gRPC\n-keep class bilibili.rpc.** { *; }\n-keep class com.google.protobuf.** { *; }\n-keep class com.google.re2j.** { *; }\n-dontwarn com.google.protobuf.GeneratedMessageV3$Builder\n-dontwarn com.google.protobuf.GeneratedMessageV3$BuilderParent\n-dontwarn com.google.protobuf.GeneratedMessageV3$FieldAccessorTable\n-dontwarn com.google.protobuf.GeneratedMessageV3\n-dontwarn com.google.protobuf.RepeatedFieldBuilderV3\n-dontwarn com.google.re2j.Matcher\n-dontwarn com.google.re2j.Pattern\n\n# kotlin-logging\n-dontwarn ch.qos.logback.classic.Level\n-dontwarn ch.qos.logback.classic.Logger\n-dontwarn ch.qos.logback.classic.LoggerContext\n-dontwarn ch.qos.logback.classic.spi.ILoggingEvent\n-dontwarn ch.qos.logback.classic.spi.LogbackServiceProvider\n-dontwarn ch.qos.logback.classic.spi.LoggingEvent\n\n# geetest\n-keep class com.geetest.sdk.** {*;}\n\n## Media3 - 保持核心类不被混淆\n#-keep class androidx.media3.common.** { *; }\n#-keep class androidx.media3.exoplayer.** { *; }\n#-keep class androidx.media3.decoder.** { *; }\n#-keep class androidx.media3.datasource.** { *; }\n#-keep class androidx.media3.ui.** { *; }\n#\n## Media3 Effect - 关键：保持视频效果相关类不被混淆\n#-keep class androidx.media3.effect.** { *; }\n#-keep interface androidx.media3.effect.** { *; }\n#\n## Media3 - 保持构造函数和 Builder 类\n#-keepclassmembers class androidx.media3.effect.*$Builder {\n#    public <init>(...);\n#    public ** build();\n#}\n#\n## Media3 - 保持视频帧处理器相关类\n#-keep class androidx.media3.effect.DefaultVideoFrameProcessor { *; }\n#-keep class androidx.media3.effect.DefaultVideoFrameProcessor$Factory { *; }\n#-keep class androidx.media3.effect.DefaultVideoFrameProcessor$Factory$Builder { *; }\n#\n## Media3 - 避免 R8 优化导致的问题\n#-keepclassmembers class * extends androidx.media3.common.Player {\n#    public <methods>;\n#}\n#\n## Media3 - 保持反射调用的方法\n#-keepclassmembers class androidx.media3.exoplayer.ExoPlayer {\n#    public void setVideoEffects(java.util.List);\n#}\n"
  },
  {
    "path": "app/shared/.gitignore",
    "content": "/build\n/src/main/res/raw/blacklist.bin"
  },
  {
    "path": "app/shared/build.gradle.kts",
    "content": "import java.net.URI\n\nplugins {\n    alias(gradleLibs.plugins.android.library)\n    alias(gradleLibs.plugins.compose.compiler)\n    alias(gradleLibs.plugins.google.ksp)\n    alias(gradleLibs.plugins.google.protobuf)\n    alias(gradleLibs.plugins.kotlin.android)\n    alias(gradleLibs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = AppConfiguration.appId\n    compileSdk = AppConfiguration.compileSdk\n\n    defaultConfig {\n        minSdk = AppConfiguration.minSdk\n        vectorDrawables {\n            useSupportLibrary = true\n        }\n\n        buildTypes {\n            buildConfigField(\n                type = \"int\",\n                name = \"VERSION_CODE\",\n                value = \"${AppConfiguration.versionCode}\"\n            )\n            buildConfigField(\n                type = \"String\",\n                name = \"VERSION_NAME\",\n                value = \"\\\"${AppConfiguration.versionName}\\\"\"\n            )\n            buildConfigField(\n                type = \"String\",\n                name = \"APPLICATION_ID\",\n                value = \"\\\"${AppConfiguration.applicationId}\\\"\"\n            )\n            buildConfigField(\n                type = \"String\",\n                name = \"BLACKLIST_URL\",\n                value = \"\\\"${AppConfiguration.blacklistUrl}\\\"\"\n            )\n        }\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"r8Test\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"alpha\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n\n    buildFeatures {\n        compose = true\n        buildConfig = true\n    }\n\n    lint {\n        targetSdk = AppConfiguration.targetSdk\n    }\n\n    testOptions {\n        targetSdk = AppConfiguration.targetSdk\n    }\n}\n\nksp {\n    arg(\"room.schemaLocation\", \"$projectDir/schemas\")\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\ndependencies {\n    annotationProcessor(androidx.room.compiler)\n    ksp(androidx.room.compiler)\n    ksp(libs.koin.ksp.compiler)\n    api(androidx.activity.compose)\n    api(androidx.core.ktx)\n    api(androidx.core.splashscreen)\n    api(androidx.compose.constraintlayout)\n    api(androidx.compose.ui)\n    api(androidx.compose.ui.util)\n    api(androidx.compose.ui.tooling.preview)\n    api(androidx.compose.material.icons)\n    api(androidx.compose.material)\n    api(androidx.compose.material3)\n    api(androidx.compose.material3.adaptive)\n    api(androidx.compose.material3.adaptive.layout)\n    api(androidx.compose.material3.adaptive.navigation)\n    api(androidx.compose.material3.adaptive.navigation.suit)\n    api(androidx.compose.material3.window.size)\n    api(androidx.compose.tv.foundation)\n    api(androidx.compose.tv.material)\n    api(androidx.datastore.typed)\n    api(androidx.datastore.preferences)\n    api(androidx.lifecycle.runtime.ktx)\n    api(androidx.media3.common)\n    api(androidx.media3.decoder)\n    api(androidx.media3.exoplayer)\n    api(androidx.media3.ui)\n    api(androidx.navigation.compose)\n    api(androidx.room.ktx)\n    api(androidx.room.runtime)\n    api(androidx.webkit)\n    api(libs.accompanist.systemuicontroller)\n//    api(project(\":akdanmaku\"))\n//    api(libs.akdanmaku)\n    api(libs.coil.compose)\n    api(libs.coil.gif)\n    api(libs.coil.svg)\n    api(libs.geetest.sensebot)\n    api(libs.koin.android)\n    api(libs.koin.annotations)\n    api(libs.koin.compose)\n    api(libs.koin.compose.navigation)\n    api(libs.kotlinx.serialization)\n    api(libs.ktor.client.cio)\n    api(libs.koin.core)\n    api(libs.ktor.client.content.negotiation)\n    api(libs.ktor.client.core)\n    api(libs.ktor.client.encoding)\n    api(libs.ktor.client.okhttp)\n    api(libs.ktor.client.serialization.kotlinx)\n    api(libs.ktor.server.cio)\n    api(libs.ktor.server.core)\n    api(libs.logging)\n    api(libs.lottie)\n    api(libs.material)\n    api(libs.protobuf.kotlin)\n    api(libs.qrcode)\n    api(libs.rememberPreference)\n    api(libs.slf4j.android.mvysny)\n    api(libs.zxing)\n    api(project(mapOf(\"path\" to \":bili-api\")))\n    api(project(mapOf(\"path\" to \":bili-subtitle\")))\n    api(project(mapOf(\"path\" to \":player\")))\n    api(project(mapOf(\"path\" to \":utils\")))\n    api(project(mapOf(\"path\" to \":symbols\")))\n    testImplementation(androidx.room.testing)\n    testImplementation(libs.kotlin.test)\n    androidTestImplementation(androidx.compose.ui.test.junit4)\n    debugApi(androidx.compose.ui.test.manifest)\n    debugApi(androidx.compose.ui.tooling)\n}\n\nprotobuf {\n    protoc {\n        artifact = \"com.google.protobuf:protoc:${libs.versions.protobuf.get()}\"\n    }\n    generateProtoTasks {\n        all().forEach { task ->\n            task.builtins {\n                create(\"java\")\n                create(\"kotlin\")\n            }\n        }\n    }\n}\n\ntasks.register(\"downloadBlacklist\") {\n    val assetsDir = file(\"src/main/res/raw\")\n    val resourceUrl = AppConfiguration.blacklistUrl\n    val outputFile = File(assetsDir, \"blacklist.bin\")\n\n    if (outputFile.exists()) return@register\n\n    doLast {\n        if (!assetsDir.exists()) {\n            assetsDir.mkdirs()\n        }\n        println(\"Downloading resource from $resourceUrl to ${outputFile.absolutePath}\")\n        URI(resourceUrl).toURL().openStream().use { input ->\n            outputFile.outputStream().use { output ->\n                input.copyTo(output)\n            }\n            println(\"Download complete: ${outputFile.absolutePath}\")\n        }\n    }\n}\n\ntasks.named(\"preBuild\").configure {\n    dependsOn(\"downloadBlacklist\")\n}"
  },
  {
    "path": "app/shared/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "app/shared/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "app/shared/schemas/dev.aaa1115910.bv.dao.AppDatabase/1.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 1,\n    \"identityHash\": \"cfa90a83d1b95f8f5ed276a5dd3120c7\",\n    \"entities\": [\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `keyword` TEXT NOT NULL, `search_date` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"keyword\",\n            \"columnName\": \"keyword\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"searchDate\",\n            \"columnName\": \"search_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cfa90a83d1b95f8f5ed276a5dd3120c7')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/shared/schemas/dev.aaa1115910.bv.dao.AppDatabase/2.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 2,\n    \"identityHash\": \"c33e0c010c4133482ddbbdce8d56428c\",\n    \"entities\": [\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `keyword` TEXT NOT NULL, `search_date` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"keyword\",\n            \"columnName\": \"keyword\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"searchDate\",\n            \"columnName\": \"search_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"user\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `uid` INTEGER NOT NULL, `username` TEXT NOT NULL, `avatar` TEXT NOT NULL, `auth` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"uid\",\n            \"columnName\": \"uid\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"username\",\n            \"columnName\": \"username\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"avatar\",\n            \"columnName\": \"avatar\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"auth\",\n            \"columnName\": \"auth\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c33e0c010c4133482ddbbdce8d56428c')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/shared/schemas/dev.aaa1115910.bv.dao.AppDatabase/3.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 3,\n    \"identityHash\": \"ad0905227bbe6c87b6048b4124cf310d\",\n    \"entities\": [\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `keyword` TEXT NOT NULL, `search_date` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"keyword\",\n            \"columnName\": \"keyword\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"searchDate\",\n            \"columnName\": \"search_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"user\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `uid` INTEGER NOT NULL, `username` TEXT NOT NULL, `avatar` TEXT NOT NULL, `auth` TEXT NOT NULL, `lock` TEXT NOT NULL DEFAULT '')\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"uid\",\n            \"columnName\": \"uid\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"username\",\n            \"columnName\": \"username\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"avatar\",\n            \"columnName\": \"avatar\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"auth\",\n            \"columnName\": \"auth\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lock\",\n            \"columnName\": \"lock\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ad0905227bbe6c87b6048b4124cf310d')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/shared/src/debug/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <application>\n    </application>\n</manifest>"
  },
  {
    "path": "app/shared/src/debug/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<resources>\n    <string name=\"app_name\">BV Debug</string>\n</resources>"
  },
  {
    "path": "app/shared/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />\n\n    <queries>\n        <package android:name=\"tw.com.gamer.android.animad\" />\n    </queries>\n\n    <application\n        android:name=\".BVApp\"\n        android:allowBackup=\"false\"\n        android:enableOnBackInvokedCallback=\"true\"\n        android:banner=\"@drawable/ic_banner\"\n        android:icon=\"@drawable/ic_launcher\"\n        android:roundIcon=\"@drawable/ic_launcher_round\"\n        android:label=\"@string/app_name\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.BV\"\n        tools:ignore=\"UnusedAttribute\">\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.provider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/provider_paths\" />\n        </provider>\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/shared/src/main/kotlin/coil/transform/BlurTransformation.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage coil.transform\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Paint\nimport android.renderscript.Allocation\nimport android.renderscript.Element\nimport android.renderscript.RenderScript\nimport android.renderscript.ScriptIntrinsicBlur\nimport androidx.annotation.RequiresApi\nimport androidx.core.graphics.applyCanvas\nimport androidx.core.graphics.createBitmap\nimport coil.size.Size\n\n/**\n * A [Transformation] that applies a Gaussian blur to an image.\n *\n * @param context The [Context] used to create a [RenderScript] instance.\n * @param radius The radius of the blur.\n * @param sampling The sampling multiplier used to scale the image. Values > 1\n *  will downscale the image. Values between 0 and 1 will upscale the image.\n */\n@RequiresApi(18)\nclass BlurTransformation @JvmOverloads constructor(\n    private val context: Context,\n    private val radius: Float = DEFAULT_RADIUS,\n    private val sampling: Float = DEFAULT_SAMPLING\n) : Transformation {\n\n    init {\n        require(radius in 0.0..25.0) { \"radius must be in [0, 25].\" }\n        require(sampling > 0) { \"sampling must be > 0.\" }\n    }\n\n    @Suppress(\"NullableToStringCall\")\n    override val cacheKey = \"${BlurTransformation::class.java.name}-$radius-$sampling\"\n\n    override suspend fun transform(input: Bitmap, size: Size): Bitmap {\n        val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)\n\n        val scaledWidth = (input.width / sampling).toInt()\n        val scaledHeight = (input.height / sampling).toInt()\n        val output = createBitmap(scaledWidth, scaledHeight, input.config!!)\n        output.applyCanvas {\n            scale(1 / sampling, 1 / sampling)\n            drawBitmap(input, 0f, 0f, paint)\n        }\n\n        var script: RenderScript? = null\n        var tmpInt: Allocation? = null\n        var tmpOut: Allocation? = null\n        var blur: ScriptIntrinsicBlur? = null\n        try {\n            script = RenderScript.create(context)\n            tmpInt = Allocation.createFromBitmap(\n                script,\n                output,\n                Allocation.MipmapControl.MIPMAP_NONE,\n                Allocation.USAGE_SCRIPT\n            )\n            tmpOut = Allocation.createTyped(script, tmpInt.type)\n            blur = ScriptIntrinsicBlur.create(script, Element.U8_4(script))\n            blur.setRadius(radius)\n            blur.setInput(tmpInt)\n            blur.forEach(tmpOut)\n            tmpOut.copyTo(output)\n        } finally {\n            script?.destroy()\n            tmpInt?.destroy()\n            tmpOut?.destroy()\n            blur?.destroy()\n        }\n\n        return output\n    }\n\n    private companion object {\n        private const val DEFAULT_RADIUS = 10f\n        private const val DEFAULT_SAMPLING = 1f\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/de/schnettler/datastore/manager/DataStoreManager.kt",
    "content": "package de.schnettler.datastore.manager\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport androidx.datastore.preferences.core.edit\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\n\nclass DataStoreManager(val dataStore: DataStore<Preferences>) {\n    val preferenceFlow = dataStore.data\n\n    suspend fun <T> getPreference(preferenceEntry: PreferenceRequest<T>) =\n        preferenceFlow.first()[preferenceEntry.key] ?: preferenceEntry.defaultValue\n\n    fun <T> getPreferenceFlow(request: PreferenceRequest<T>) =\n        preferenceFlow.map {\n            it[request.key] ?: request.defaultValue\n        }\n\n    @Composable\n    fun <T> getPreferenceState(request: PreferenceRequest<T>) =\n        getPreferenceFlow(request).collectAsState(\n            initial = request.defaultValue\n        )\n\n    suspend fun <T> editPreference(key: Preferences.Key<T>, newValue: T) {\n        dataStore.edit { preferences ->\n            preferences[key] = newValue\n        }\n    }\n\n    suspend fun clearPreferences() {\n        dataStore.edit { preferences -> preferences.clear() }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/de/schnettler/datastore/manager/PreferenceRequest.kt",
    "content": "package de.schnettler.datastore.manager\n\nimport androidx.datastore.preferences.core.Preferences\n\nopen class PreferenceRequest<T>(\n    val key: Preferences.Key<T>,\n    val defaultValue: T,\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/BVApp.kt",
    "content": "package dev.aaa1115910.bv\n\nimport android.annotation.SuppressLint\nimport android.app.Application\nimport android.content.Context\nimport android.os.Build\nimport android.util.Log\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport androidx.datastore.preferences.preferencesDataStore\nimport androidx.webkit.WebViewCompat\nimport coil.Coil\nimport de.schnettler.datastore.manager.DataStoreManager\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.biliapi.http.BiliHttpProxyApi\nimport dev.aaa1115910.biliapi.http.util.BiliAppConf\nimport dev.aaa1115910.biliapi.http.util.BiliDns\nimport dev.aaa1115910.biliapi.http.util.BiliWebConf\nimport dev.aaa1115910.biliapi.repositories.AuthRepository\nimport dev.aaa1115910.biliapi.repositories.BiliApiModule\nimport dev.aaa1115910.biliapi.repositories.ChannelRepository\nimport dev.aaa1115910.bv.dao.AppDatabase\nimport dev.aaa1115910.bv.entity.AuthData\nimport dev.aaa1115910.bv.entity.db.UserDB\nimport dev.aaa1115910.bv.network.HttpServer\nimport dev.aaa1115910.bv.util.BlacklistUtil\nimport dev.aaa1115910.bv.util.CoilConfig\nimport dev.aaa1115910.bv.util.LogCatcherUtil\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.toast\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.android.ext.koin.androidLogger\nimport org.koin.core.KoinApplication\nimport org.koin.core.annotation.ComponentScan\nimport org.koin.core.annotation.Module\nimport org.koin.core.context.startKoin\nimport org.koin.core.logger.Level\nimport org.koin.ksp.generated.module\nimport org.slf4j.impl.HandroidLoggerAdapter\n\nclass BVApp : Application() {\n    companion object {\n        @SuppressLint(\"StaticFieldLeak\")\n        lateinit var context: Context\n        lateinit var dataStoreManager: DataStoreManager\n        lateinit var koinApplication: KoinApplication\n        var instance: BVApp? = null\n\n        fun getAppDatabase(context: Context = this.context) = AppDatabase.getDatabase(context)\n    }\n\n    override fun onCreate() {\n        super.onCreate()\n        context = this.applicationContext\n        HandroidLoggerAdapter.DEBUG = BuildConfig.DEBUG\n        dataStoreManager = DataStoreManager(applicationContext.dataStore)\n        if (Prefs.blacklistUser) {\n            R.string.blacklist_user_toast.toast(context)\n            return\n        }\n        koinApplication = startKoin {\n            androidLogger(if (BuildConfig.DEBUG) Level.ERROR else Level.NONE)\n            androidContext(this@BVApp)\n            modules(AppModule().module)\n        }\n        initCoil()\n        LogCatcherUtil.installLogCatcher()\n        initApiConfig()\n        initDns()\n        initRepository()\n        initProxy()\n        instance = this\n        updateMigration()\n        HttpServer.startServer()\n        updateBlacklist()\n    }\n\n    /**\n     * 初始化 Coil 图片加载库\n     * 使用优化后的 ImageLoader 配置，支持多线程并发加载\n     */\n    private fun initCoil() {\n        Coil.setImageLoader(CoilConfig.createImageLoader(this))\n    }\n\n    private fun initApiConfig() {\n        BiliAppConf.osVersion = Build.VERSION.RELEASE\n        BiliAppConf.model = Build.MODEL\n        // 设置 sessData 提供者，用于更新 WBI keys 时携带登录凭证\n        BiliHttpApi.sessDataProvider = { Prefs.sessData }\n        BiliHttpApi.buvid3Provider = { Prefs.buvid3 }\n        BiliWebConf.webViewVersion = runCatching {\n            WebViewCompat.getCurrentLoadedWebViewPackage()!!.versionName!!\n                .substringBefore(\".\").toInt()\n        }.getOrDefault(144)\n    }\n\n    private fun initDns() {\n        BiliDns.ipv4Only = Prefs.ipv4Only\n    }\n\n    fun initRepository() {\n        val channelRepository by koinApplication.koin.inject<ChannelRepository>()\n        channelRepository.initDefaultChannel(Prefs.accessToken, Prefs.buvid)\n\n        val authRepository by koinApplication.koin.inject<AuthRepository>()\n        authRepository.sessionData = Prefs.sessData.takeIf { it.isNotEmpty() }\n        authRepository.biliJct = Prefs.biliJct.takeIf { it.isNotEmpty() }\n        authRepository.accessToken = Prefs.accessToken.takeIf { it.isNotEmpty() }\n        authRepository.mid = Prefs.uid.takeIf { it != 0L }\n        authRepository.buvid3 = Prefs.buvid3\n        authRepository.buvid = Prefs.buvid\n    }\n\n    fun initProxy() {\n        if (Prefs.enableProxy) {\n            BiliHttpProxyApi.createClient(Prefs.proxyHttpServer)\n\n            val channelRepository by koinApplication.koin.inject<ChannelRepository>()\n            runCatching {\n                channelRepository.initProxyChannel(\n                    Prefs.accessToken,\n                    Prefs.buvid,\n                    Prefs.proxyGRPCServer\n                )\n            }\n        }\n    }\n\n    private fun updateMigration() {\n        val lastVersionCode = Prefs.lastVersionCode\n        if (lastVersionCode >= BuildConfig.VERSION_CODE) return\n        Log.i(\"BVApp\", \"updateMigration from $lastVersionCode\")\n        if (lastVersionCode < 576) {\n            // 从 Prefs 中读取登录数据写入 UserDB\n            if (Prefs.isLogin) {\n                runBlocking {\n                    val existedUser = getAppDatabase().userDao().findUserByUid(Prefs.uid)\n                    if (existedUser == null) {\n                        val user = UserDB(\n                            uid = Prefs.uid,\n                            username = \"Unknown\",\n                            avatar = \"\",\n                            auth = AuthData.fromPrefs().toJson()\n                        )\n                        getAppDatabase().userDao().insert(user)\n                    }\n                }\n            }\n        }\n        Prefs.lastVersionCode = BuildConfig.VERSION_CODE\n    }\n\n    private fun updateBlacklist() {\n        CoroutineScope(Dispatchers.IO).launch {\n            BlacklistUtil.updateBlacklist(context)\n            BlacklistUtil.checkUid(Prefs.uid)\n        }\n    }\n}\n\nval Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = \"Settings\")\n\n@Module(includes = [BiliApiModule::class])\n@ComponentScan\nclass AppModule"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/activities/LauncherActivity.kt",
    "content": "package dev.aaa1115910.bv.activities\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport dev.aaa1115910.bv.entity.InterfaceMode\nimport dev.aaa1115910.bv.util.DeviceUtil\nimport dev.aaa1115910.bv.util.Prefs\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\n/**\n * 启动器活动\n * \n * 这个活动是应用的入口点，它会根据设备类型路由到合适的主活动\n */\nclass LauncherActivity : ComponentActivity() {\n    private val logger = KotlinLogging.logger { }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        Prefs.currentPlaySpeed = Prefs.defaultPlaySpeed\n        routeToCorrectActivity()\n    }\n    \n    /**\n     * 根据设备类型路由到正确的活动\n     */\n    private fun routeToCorrectActivity() {\n        val interfaceMode = Prefs.interfaceMode\n        val shouldLaunchTv = when (interfaceMode) {\n            InterfaceMode.Auto -> DeviceUtil.isTvDevice(this)\n            InterfaceMode.TV -> true\n            InterfaceMode.Mobile -> false\n        }\n\n        val intent = if (shouldLaunchTv) {\n            logger.info { \"Launching TV MainActivity, interfaceMode=$interfaceMode\" }\n            Intent(this, Class.forName(\"dev.aaa1115910.bv.tv.activities.MainActivity\"))\n        } else {\n            logger.info { \"Launching Mobile MainActivity, interfaceMode=$interfaceMode\" }\n            Intent(this, Class.forName(\"dev.aaa1115910.bv.mobile.activities.MainActivity\"))\n        }\n\n        // 传递原始Intent中的所有数据\n        intent.putExtras(getIntent())\n\n        // 启动相应的MainActivity\n        startActivity(intent)\n\n        // 关闭当前Activity\n        finish()\n    }\n\n    companion object {\n        /**\n         * 清空当前任务栈并重新走启动路由，用于界面模式切换后立即生效\n         */\n        fun actionRestart(context: Context) {\n            val intent = Intent(context, LauncherActivity::class.java).apply {\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)\n            }\n            context.startActivity(intent)\n        }\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/component/BvPlayerPreview.kt",
    "content": "package dev.aaa1115910.bv.component\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport dev.aaa1115910.bv.player.AbstractVideoPlayer\nimport dev.aaa1115910.bv.player.BvVideoPlayer\nimport dev.aaa1115910.bv.player.VideoPlayerListener\nimport dev.aaa1115910.bv.player.VideoPlayerOptions\nimport dev.aaa1115910.bv.player.impl.exo.ExoPlayerFactory\n\nprivate const val videoUrl = \"\"\nprivate const val audioUrl = \"\"\n\nprivate val options = VideoPlayerOptions(\n    userAgent = dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_WEB,\n    referer = \"https://www.bilibili.com/\"\n)\n\nprivate val videoPlayerListener = object : VideoPlayerListener {\n    override fun onError(error: Exception) {\n        println(\"onError: $error\")\n        //TODO(\"Not yet implemented\")\n    }\n\n    override fun onReady() {\n        println(\"onReady\")\n        //TODO(\"Not yet implemented\")\n    }\n\n    override fun onPlay() {\n        println(\"onPlay\")\n        //TODO(\"Not yet implemented\")\n    }\n\n    override fun onPause() {\n        println(\"onPause\")\n        //TODO(\"Not yet implemented\")\n    }\n\n    override fun onBuffering() {\n        println(\"onBuffering\")\n        //TODO(\"Not yet implemented\")\n    }\n\n    override fun onEnd() {\n        println(\"onEnd\")\n        //TODO(\"Not yet implemented\")\n    }\n\n    override fun onIdle() {\n        println(\"onIdle\")\n        //TODO(\"Not yet implemented\")\n    }\n\n    override fun onSeekBack(seekBackIncrementMs: Long) {\n        //TODO(\"Not yet implemented\")\n    }\n\n    override fun onSeekForward(seekForwardIncrementMs: Long) {\n        //TODO(\"Not yet implemented\")\n    }\n}\n\n@Preview\n@Composable\nfun BvVideoPlayerExoPreview() {\n    val context = LocalContext.current\n    val exoPlayer by remember { mutableStateOf(ExoPlayerFactory().create(context, options)) }\n\n    BvVideoPlayerPreview(exoPlayer)\n}\n\n@Composable\nfun BvVideoPlayerPreview(\n    player: AbstractVideoPlayer\n) {\n    LaunchedEffect(Unit) {\n        player.setOptions()\n        player.playUrl(videoUrl, audioUrl)\n        player.prepare()\n    }\n\n    BvVideoPlayer(\n        modifier = Modifier.fillMaxSize(),\n        videoPlayer = player,\n        playerListener = videoPlayerListener\n    )\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/component/DevelopingTip.kt",
    "content": "package dev.aaa1115910.bv.component\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun DevelopingTip(modifier: Modifier = Modifier) {\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(20.dp)\n    ) {\n        Text(\n            text = \"\\uD83D\\uDEA7\",\n            style = MaterialTheme.typography.displayLarge\n        )\n        Text(\n            text = \"前方施工 请绕行\",\n            style = MaterialTheme.typography.titleLarge\n        )\n    }\n}\n\n@Composable\nfun DevelopingTipContent(modifier: Modifier = Modifier) {\n    Box(\n        modifier = modifier\n            .fillMaxSize(),\n        contentAlignment = Alignment.Center\n    ) {\n        DevelopingTip()\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun DevelopingTipPreview() {\n    BVTheme {\n        DevelopingTip()\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun DevelopingTipContentPreview() {\n    BVTheme {\n        DevelopingTipContent()\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/component/FpsMonitor.kt",
    "content": "package dev.aaa1115910.bv.component\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.withFrameMillis\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\n\n@Composable\nfun FpsMonitor(\n    modifier: Modifier = Modifier,\n    content: @Composable () -> Unit\n) {\n    var fpsCount by remember { mutableIntStateOf(0) }\n    var fps by remember { mutableIntStateOf(0) }\n    var lastUpdate by remember { mutableLongStateOf(0L) }\n\n    LaunchedEffect(Unit) {\n        while (true) {\n            withFrameMillis { ms ->\n                fpsCount++\n                if (fpsCount == 5) {\n                    fps = (5000 / (ms - lastUpdate)).toInt()\n                    lastUpdate = ms\n                    fpsCount = 0\n                }\n            }\n        }\n    }\n\n    Box(\n        modifier = modifier.fillMaxSize()\n    ) {\n        content()\n        Text(\n            modifier = modifier\n                .size(60.dp)\n                .align(Alignment.TopStart),\n            text = \"Fps: $fps\",\n            color = Color.Red,\n            style = MaterialTheme.typography.bodyMedium\n        )\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/component/QrImage.kt",
    "content": "package dev.aaa1115910.bv.component\n\nimport android.graphics.BitmapFactory\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.ImageBitmapConfig\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.util.countDownTimer\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport qrcode.QRCode\nimport java.io.ByteArrayInputStream\nimport java.io.ByteArrayOutputStream\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun QrImage(\n    modifier: Modifier = Modifier,\n    content: String,\n    borderWidth: Dp = 24.dp,\n    shape: Shape = MaterialTheme.shapes.large,\n    showLoadingWhenContentChanged: Boolean = true\n) {\n    val scope = rememberCoroutineScope()\n    var qrImage by remember { mutableStateOf(ImageBitmap(1, 1, ImageBitmapConfig.Argb8888)) }\n    var qrGenerated by remember { mutableStateOf(false) }\n    var qrJob by remember { mutableStateOf<Job?>(null) }\n\n    val createQr: suspend () -> Unit = {\n        val output = ByteArrayOutputStream()\n        QRCode(content).render().writeImage(output)\n        val input = ByteArrayInputStream(output.toByteArray())\n        val image = BitmapFactory.decodeStream(input).asImageBitmap()\n        withContext(Dispatchers.Main) { qrImage = image }\n        //delay(2000)\n        qrGenerated = true\n    }\n\n    LaunchedEffect(content) {\n        qrJob?.cancel()\n        qrJob = scope.launch(Dispatchers.Default) {\n            if (showLoadingWhenContentChanged) qrGenerated = false\n            if (content.isNotBlank()) createQr()\n        }\n    }\n\n    Box(\n        modifier = modifier\n            .clip(shape)\n            .background(Color.White),\n        contentAlignment = Alignment.Center,\n    ) {\n        if (qrGenerated) {\n            Image(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(borderWidth),\n                bitmap = qrImage,\n                contentDescription = null\n            )\n        } else {\n            LoadingIndicator(\n                modifier = Modifier.fillMaxSize(0.5f)\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun QrImagePreview() {\n    MaterialTheme {\n        QrImage(\n            modifier = Modifier.size(240.dp),\n            content = \"https://www.example.com\"\n        )\n    }\n}\n\n\n@Preview\n@Composable\nprivate fun QrImageWithContentsPreview() {\n    val contents = listOf(\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"10\")\n    var currentContent by remember { mutableStateOf(\"\") }\n    var index by remember { mutableIntStateOf(0) }\n\n    LaunchedEffect(Unit) {\n        countDownTimer(\n            millisInFuture = 1000L * 10,\n            countDownInterval = 1000L,\n            tag = \"\",\n            onTick = { _ ->\n                currentContent = contents[index++]\n                println(\"Current content: $currentContent\")\n            },\n        )\n    }\n\n    MaterialTheme {\n        Column {\n            QrImage(\n                modifier = Modifier.size(240.dp),\n                content = currentContent\n            )\n        }\n    }\n}\n\n\n@Preview\n@Composable\nprivate fun QrImageDifferentSizesPreview() {\n    MaterialTheme {\n        Column(\n            modifier = Modifier.verticalScroll(rememberScrollState())\n        ) {\n            QrImage(\n                modifier = Modifier.size(100.dp),\n                content = \"https://www.example.com\",\n                borderWidth = 16.dp\n            )\n            QrImage(\n                modifier = Modifier.size(200.dp),\n                content = \"https://www.example.com\"\n            )\n            QrImage(\n                modifier = Modifier.size(300.dp),\n                content = \"https://www.example.com\"\n            )\n            QrImage(\n                modifier = Modifier.size(400.dp),\n                content = \"https://www.example.com\"\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/component/settings/UpdateDialog.kt",
    "content": "package dev.aaa1115910.bv.component.settings\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.res.Configuration\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.gestures.animateScrollBy\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.tooling.preview.PreviewParameter\nimport androidx.compose.ui.tooling.preview.PreviewParameterProvider\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.core.content.FileProvider\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.ProvideTextStyle\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.network.GithubApi\nimport dev.aaa1115910.bv.network.entity.Release\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.fException\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toMBString\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.ktor.client.content.ProgressListener\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport java.io.File\nimport java.util.UUID\n\n@Composable\nfun UpdateDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    text: @Composable ((text: String) -> Unit),\n    button: @Composable ((enabled: Boolean, onClick: () -> Unit, content: @Composable (RowScope.() -> Unit)) -> Unit),\n    outlinedButton: @Composable ((enabled: Boolean, onClick: () -> Unit, content: @Composable (RowScope.() -> Unit)) -> Unit)\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger(\"UpdateDialog\")\n\n    var updateStatus by remember { mutableStateOf(UpdateStatus.UpdatingInfo) }\n\n    var bytesSentTotal: Long by remember { mutableLongStateOf(0L) }\n    var contentLength: Long by remember { mutableLongStateOf(0L) }\n    var targetProgress by remember { mutableFloatStateOf(0f) }\n    val progress by animateFloatAsState(\n        targetValue = targetProgress,\n        label = \"update progress\"\n    )\n\n    var latestReleaseBuild by remember { mutableStateOf<Release?>(null) }\n    var updateReleases by remember { mutableStateOf<List<Release>>(emptyList()) }\n\n    var downloadJob by remember { mutableStateOf<Job?>(null) }\n    DisposableEffect(show) {\n        if(!show) {\n            downloadJob?.cancel()\n        }\n        onDispose {\n            downloadJob?.cancel()\n        }\n    }\n    val checkUpdate: () -> Unit = {\n        updateStatus = UpdateStatus.UpdatingInfo\n\n        scope.launch(Dispatchers.IO) {\n            runCatching {\n                latestReleaseBuild = GithubApi.getLatestBuild()\n                val name = latestReleaseBuild!!\n                    .assets.first { it.name.startsWith(\"BV\") }\n                    .name\n                val revision = name.split(\"_\")[1].toInt()\n                updateReleases = GithubApi.getUpdateReleases(\n                    BuildConfig.VERSION_CODE, BuildConfig.VERSION_NAME\n                )\n                if (revision < BuildConfig.VERSION_CODE || name.contains(BuildConfig.VERSION_NAME)) {\n                    updateStatus = UpdateStatus.NoAvailableUpdate\n                    return@launch\n                }\n            }.onFailure {\n                logger.fException(it) { \"Failed to get latest version\" }\n                updateStatus = UpdateStatus.CheckError\n            }.onSuccess {\n                logger.fInfo { \"Find latest version ${latestReleaseBuild!!.name}\" }\n                updateStatus = UpdateStatus.Ready\n            }\n        }\n    }\n\n    val installUpdate: (File) -> Unit = { file ->\n        updateStatus = UpdateStatus.Installing\n        runCatching {\n            val uri =\n                FileProvider.getUriForFile(context, \"${BuildConfig.APPLICATION_ID}.provider\", file)\n            val intent = Intent(Intent.ACTION_VIEW).apply {\n                addCategory(Intent.CATEGORY_DEFAULT)\n                setDataAndType(uri, \"application/vnd.android.package-archive\")\n                flags = Intent.FLAG_ACTIVITY_NEW_TASK\n                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n            }\n            context.startActivity(intent)\n        }.onFailure {\n            updateStatus = UpdateStatus.InstallError\n        }\n    }\n\n    val startUpdate: () -> Unit = {\n        updateStatus = UpdateStatus.Downloading\n        downloadJob = scope.launch(Dispatchers.IO) {\n            val tempFilename = \"${UUID.randomUUID()}.apk\"\n            val tempDir = File(context.cacheDir, \"update_downloader\")\n            if (!tempDir.exists()) tempDir.mkdirs()\n            val tempFile = File(tempDir, tempFilename)\n            tempFile.createNewFile()\n            runCatching {\n                GithubApi.downloadUpdate(\n                    latestReleaseBuild!!,\n                    tempFile,\n                    object : ProgressListener {\n                        override suspend fun onProgress(downloaded: Long, total: Long?) {\n                            bytesSentTotal = downloaded\n                            contentLength = total ?: 0\n                            targetProgress =\n                                runCatching { bytesSentTotal.toFloat() / contentLength }\n                                    .getOrDefault(0f)\n                        }\n                    })\n                if (show) installUpdate(tempFile)\n            }.onFailure {\n                logger.fException(it) { \"Failed to download update\" }\n                updateStatus = UpdateStatus.DownloadError\n            }\n        }\n    }\n\n    LaunchedEffect(show) {\n        if (show) {\n            checkUpdate()\n        } else {\n            updateStatus = UpdateStatus.UpdatingInfo\n        }\n    }\n\n    if (show) {\n        UpdateDialogContent(\n            modifier = modifier,\n            updateStatus = updateStatus,\n            latestReleaseBuild = latestReleaseBuild,\n            updateReleases = updateReleases,\n            progress = progress,\n            bytesSentTotal = bytesSentTotal,\n            contentLength = contentLength,\n            onHideDialog = onHideDialog,\n            checkUpdate = checkUpdate,\n            startUpdate = startUpdate,\n            text = text,\n            button = button,\n            outlinedButton = outlinedButton\n        )\n    }\n}\n\n@SuppressLint(\"ConfigurationScreenWidthHeight\")\n@Composable\nprivate fun UpdateDialogContent(\n    modifier: Modifier = Modifier,\n    updateStatus: UpdateStatus,\n    latestReleaseBuild: Release?,\n    updateReleases: List<Release> = emptyList(),\n    progress: Float,\n    bytesSentTotal: Long,\n    contentLength: Long,\n    onHideDialog: () -> Unit,\n    checkUpdate: () -> Unit,\n    startUpdate: () -> Unit,\n    text: @Composable ((text: String) -> Unit),\n    button: @Composable ((enabled: Boolean, onClick: () -> Unit, content: @Composable (RowScope.() -> Unit)) -> Unit),\n    outlinedButton: @Composable ((enabled: Boolean, onClick: () -> Unit, content: @Composable (RowScope.() -> Unit)) -> Unit)\n) {\n    val configuration = LocalConfiguration.current\n    val maxHeight = (configuration.screenHeightDp * 0.8).dp\n    val weight = (configuration.screenWidthDp * 0.67).dp\n    val scrollState = rememberScrollState()\n    val scope = rememberCoroutineScope()\n    val confirmButtonFocusRequester = remember { FocusRequester() }\n\n    AlertDialog(\n        modifier = modifier\n            .width(weight)\n            .heightIn(max = maxHeight),\n        onDismissRequest = { onHideDialog() },\n        title = {\n            ProvideTextStyle(\n                value = MaterialTheme.typography.headlineSmall\n            ) {\n                text(\n                    when (updateStatus) {\n                        UpdateStatus.UpdatingInfo -> \"获取更新信息中\"\n                        UpdateStatus.Ready -> \"更新内容\"\n                        UpdateStatus.Downloading -> \"下载中\"\n                        UpdateStatus.Installing -> \"安装中\"\n                        UpdateStatus.NoAvailableUpdate -> \"无可用更新 | 当前版本内容\"\n                        UpdateStatus.CheckError -> \"检查更新失败\"\n                        UpdateStatus.DownloadError -> \"下载失败\"\n                        UpdateStatus.InstallError -> \"安装失败\"\n                    }\n                )\n            }\n        },\n        text = {\n            Column(\n                modifier = Modifier\n                    .verticalScroll(scrollState)\n                    .focusable()\n                    .onKeyEvent { keyEvent ->\n                        when (keyEvent.key) {\n                            Key.DirectionUp -> {\n                                scope.launch {\n                                    scrollState.animateScrollBy(-100f)\n                                }\n                                true\n                            }\n                            Key.DirectionDown -> {\n                                scope.launch {\n                                    val canScrollDown = scrollState.value < scrollState.maxValue\n                                    if (canScrollDown) {\n                                        scrollState.animateScrollBy(100f)\n                                    } else {\n                                        // 已经滚动到底部，将焦点转移到确认按钮\n                                        confirmButtonFocusRequester.requestFocus()\n                                    }\n                                }\n                                true\n                            }\n                            else -> false\n                        }\n                    }\n            ) {\n                when (updateStatus) {\n                    UpdateStatus.UpdatingInfo -> text(\"检查更新中...\")\n                    UpdateStatus.Ready -> {\n                        val changelogText = updateReleases.joinToString(\"\\n\\n\") {\n                            val prefix = if (it.prerelease) \"Pre-release\" else \"Release\"\n                            \"# $prefix ${it.name}\\n${it.body ?: \"\"}\"\n                        }.ifEmpty { \"Empty content\" }\n                        text(changelogText)\n                    }\n                    UpdateStatus.Downloading -> Column(\n                        verticalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        LinearProgressIndicator(\n                            progress = { progress },\n                            modifier = Modifier.fillMaxWidth(),\n                            gapSize = 0.dp,\n                        )\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            horizontalArrangement = Arrangement.End\n                        ) {\n                            text(\"${bytesSentTotal.toMBString()}/${contentLength.toMBString()}\")\n                        }\n                    }\n\n                    UpdateStatus.Installing -> text(\"请坐和放宽\")\n                    UpdateStatus.DownloadError -> text(\"下载失败\")\n                    UpdateStatus.InstallError -> text(\"安装失败\")\n                    UpdateStatus.CheckError -> text(\"获取更新信息失败\")\n                    UpdateStatus.NoAvailableUpdate -> {\n                        val release = latestReleaseBuild\n                        if (release != null) {\n                            val prefix = if (release.prerelease) \"Pre-release\" else \"Release\"\n                            text(\"# $prefix ${release.name}\\n${release.body ?: \"\"}\")\n                        } else {\n                            text(\"真没更新，骗你是小狗！\")\n                        }\n                    }\n                }\n            }\n        },\n        confirmButton = {\n            when (updateStatus) {\n                UpdateStatus.UpdatingInfo, UpdateStatus.NoAvailableUpdate, UpdateStatus.Downloading, UpdateStatus.Installing -> {}\n\n                UpdateStatus.Ready -> {\n                    Box(modifier = Modifier.focusRequester(confirmButtonFocusRequester)) {\n                        button(true, startUpdate) {\n                            text(\"立即更新\")\n                        }\n                    }\n                }\n\n                UpdateStatus.InstallError, UpdateStatus.DownloadError, UpdateStatus.CheckError -> {\n                    Box(modifier = Modifier.focusRequester(confirmButtonFocusRequester)) {\n                        button(true, checkUpdate) {\n                            text(\"再试一次\")\n                        }\n                    }\n                }\n            }\n        },\n        dismissButton = {\n            Box(modifier = Modifier.focusRequester(confirmButtonFocusRequester)) {\n                outlinedButton(\n                    !(updateStatus == UpdateStatus.Downloading || updateStatus == UpdateStatus.Installing),\n                    onHideDialog\n                ) {\n                    text(\n                        when (updateStatus) {\n                            UpdateStatus.UpdatingInfo -> \"我点错了\"\n                            UpdateStatus.Ready -> \"打死不更\"\n                            UpdateStatus.NoAvailableUpdate -> \"走了走了\"\n                            UpdateStatus.CheckError, UpdateStatus.DownloadError, UpdateStatus.InstallError -> \"算了算了\"\n                            UpdateStatus.Downloading, UpdateStatus.Installing -> \"你已经无路可逃！\"\n                        }\n                    )\n                }\n            }\n        },\n        properties = DialogProperties(usePlatformDefaultWidth = false)\n    )\n}\n\nenum class UpdateStatus {\n    UpdatingInfo, Ready, Downloading, Installing,\n    NoAvailableUpdate, CheckError, DownloadError, InstallError\n}\n\nprivate class UpdateStatusProvider : PreviewParameterProvider<UpdateStatus> {\n    override val values = UpdateStatus.entries.asSequence()\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun UpdateDialogPreview(\n    @PreviewParameter(UpdateStatusProvider::class) updateStatus: UpdateStatus\n) {\n    BVTheme {\n        UpdateDialogContent(\n            updateStatus = updateStatus,\n            latestReleaseBuild = Release(\n                name = \"BV-1.0.0\",\n                body = \"测试更新\",\n                assets = emptyList(),\n                tagName = \"v1.0.0\",\n                publishedAt = \"2023-10-01T00:00:00Z\",\n                url = \"\",\n                uploadUrl = \"\",\n                assetsUrl = \"\",\n                author = Release.User(\n                    login = \"\",\n                    id = 1,\n                    nodeId = \"\",\n                    avatarUrl = \"\",\n                    gravatarId = \"\",\n                    url = \"\",\n                    htmlUrl = \"\",\n                    followersUrl = \"\",\n                    followingUrl = \"\",\n                    gistsUrl = \"\",\n                    starredUrl = \"\",\n                    subscriptionsUrl = \"\",\n                    organizationsUrl = \"\",\n                    reposUrl = \"\",\n                    eventsUrl = \"\",\n                    receivedEventsUrl = \"\",\n                    type = \"\",\n                    siteAdmin = false\n                ),\n                draft = false,\n                htmlUrl = \"\",\n                createdAt = \"2023-10-01T00:00:00Z\",\n                nodeId = \"\",\n                id = 0,\n                prerelease = false,\n                reactions = null,\n                tarballUrl = \"\",\n                targetCommitish = \"\",\n                zipballUrl = \"\",\n            ),\n            progress = 0.5f,\n            bytesSentTotal = 100L,\n            contentLength = 200L,\n            onHideDialog = {},\n            checkUpdate = {},\n            startUpdate = {},\n            text = { Text(text = it) },\n            button = { enabled, onClick, content ->\n                Button(\n                    enabled = enabled,\n                    onClick = onClick,\n                    content = content\n                )\n            },\n            outlinedButton = { enabled, onClick, content ->\n                OutlinedButton(\n                    enabled = enabled,\n                    onClick = onClick,\n                    content = content\n                )\n            }\n        )\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/dao/AppDatabase.kt",
    "content": "package dev.aaa1115910.bv.dao\n\nimport android.content.Context\nimport androidx.room.AutoMigration\nimport androidx.room.Database\nimport androidx.room.Room\nimport androidx.room.RoomDatabase\nimport androidx.room.TypeConverter\nimport androidx.room.TypeConverters\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.entity.db.SearchHistoryDB\nimport dev.aaa1115910.bv.entity.db.UserDB\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport java.util.Date\nimport java.util.concurrent.Executors\n\n@Database(\n    entities = [SearchHistoryDB::class, UserDB::class],\n    version = 3,\n    exportSchema = true,\n    autoMigrations = [\n        AutoMigration(from = 1, to = 2),\n        AutoMigration(from = 2, to = 3)\n    ]\n)\n@TypeConverters(Converters::class)\nabstract class AppDatabase : RoomDatabase() {\n    abstract fun searchHistoryDao(): SearchHistoryDao\n    abstract fun userDao(): UserDao\n\n    companion object {\n        private var instance: AppDatabase? = null\n        private val logger = KotlinLogging.logger { }\n\n        @Suppress(\"unused\")\n        fun reset() {\n            instance = null\n        }\n\n        @Synchronized\n        fun getDatabase(context: Context): AppDatabase {\n            instance?.let { return it }\n            return Room.databaseBuilder(\n                context.applicationContext,\n                AppDatabase::class.java,\n                \"AppDatabase.db\"\n            )\n                .setQueryCallback(object : QueryCallback {\n                    override fun onQuery(sqlQuery: String, bindArgs: List<Any?>) {\n                        if (BuildConfig.DEBUG) logger.info { \"SQL Query: $sqlQuery SQL Args: $bindArgs\" }\n                    }\n                }, Executors.newSingleThreadExecutor())\n                .build()\n                .apply { instance = this }\n        }\n    }\n}\n\nobject Converters {\n    @TypeConverter\n    fun timestampToDate(value: Long?): Date? = value?.let { Date(it) }\n\n    @TypeConverter\n    fun dateToTimestamp(date: Date?): Long? = date?.time\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/dao/SearchHistoryDao.kt",
    "content": "package dev.aaa1115910.bv.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.Query\nimport androidx.room.Update\nimport dev.aaa1115910.bv.entity.db.SearchHistoryDB\n\n@Dao\ninterface SearchHistoryDao {\n    @Query(\"SELECT * FROM search_history\")\n    suspend fun getAll(): List<SearchHistoryDB>\n\n    @Query(\"SELECT * FROM search_history ORDER BY search_date DESC LIMIT :count\")\n    suspend fun getHistories(count: Int): List<SearchHistoryDB>\n\n    @Query(\"SELECT * FROM search_history WHERE keyword = :keyword LIMIT 1\")\n    suspend fun findHistory(keyword: String): SearchHistoryDB?\n\n    @Query(\"SELECT * FROM search_history WHERE keyword LIKE '%' || :keyword || '%' LIMIT :count\")\n    suspend fun findHistories(keyword: String, count: Int): List<SearchHistoryDB>\n\n    @Insert\n    suspend fun insert(vararg searchHistoryDB: SearchHistoryDB)\n\n    @Delete\n    suspend fun delete(vararg searchHistoryDB: SearchHistoryDB)\n\n    @Query(\"DELETE FROM search_history\")\n    suspend fun deleteAll()\n\n    @Update\n    suspend fun update(searchHistoryDB: SearchHistoryDB)\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/dao/UserDao.kt",
    "content": "package dev.aaa1115910.bv.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.Query\nimport androidx.room.Update\nimport dev.aaa1115910.bv.entity.db.UserDB\n\n@Dao\ninterface UserDao {\n    @Query(\"SELECT * FROM user\")\n    suspend fun getAll(): List<UserDB>\n\n    @Query(\"SELECT * FROM user WHERE uid = :uid LIMIT 1\")\n    suspend fun findUserByUid(uid: Long): UserDB?\n\n    @Insert\n    suspend fun insert(vararg userDB: UserDB)\n\n    @Delete\n    suspend fun delete(vararg userDB: UserDB)\n\n    @Update\n    suspend fun update(userDB: UserDB)\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/AuthData.kt",
    "content": "package dev.aaa1115910.bv.entity\n\nimport dev.aaa1115910.bv.util.Prefs\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.encodeToString\nimport kotlinx.serialization.json.Json\nimport java.util.Date\n\n@Serializable\ndata class AuthData(\n    @SerialName(\"DedeUserID\")\n    val uid: Long,\n    @SerialName(\"DedeUserID__ckMd5\")\n    val uidCkMd5: String,\n    val sid: String,\n    @SerialName(\"bili_jct\")\n    val biliJct: String,\n    @SerialName(\"SESSDATA\")\n    val sessData: String,\n    @SerialName(\"expired_date\")\n    val tokenExpiredData: Long,\n    @SerialName(\"access_token\")\n    val accessToken: String = \"\",\n    @SerialName(\"refresh_token\")\n    val refreshToken: String = \"\"\n) {\n    companion object {\n        fun fromJson(json: String): AuthData {\n            return Json.decodeFromString(json)\n        }\n\n        fun fromPrefs(): AuthData {\n            return AuthData(\n                Prefs.uid,\n                Prefs.uidCkMd5,\n                Prefs.sid,\n                Prefs.biliJct,\n                Prefs.sessData,\n                Prefs.tokenExpiredData.time,\n                Prefs.accessToken,\n                Prefs.refreshToken\n            )\n        }\n    }\n\n    fun toJson(): String = Json.encodeToString(this)\n    fun saveToPrefs() {\n        Prefs.uid = uid\n        Prefs.uidCkMd5 = uidCkMd5\n        Prefs.sid = sid\n        Prefs.biliJct = biliJct\n        Prefs.sessData = sessData\n        Prefs.tokenExpiredData = Date(tokenExpiredData)\n        Prefs.accessToken = accessToken\n        Prefs.refreshToken = refreshToken\n        Prefs.isLogin = true\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/BvScheme.kt",
    "content": "package dev.aaa1115910.bv.entity\n\nimport android.net.Uri\n\nsealed class BvScheme(open val host: String) {\n    companion object {\n        private const val SCHEME = \"bugvideo\"\n    }\n\n    open fun buildUri(): String {\n        return \"bv://${host}\"\n    }\n\n    data class QrToken(\n        val uid: Long,\n        val username: String,\n        val avatar: String,\n        val auth: String,\n    ) : BvScheme(HOST) {\n        companion object {\n            const val HOST = \"qrtoken\"\n\n            fun fromUri(uri: Uri): BvScheme? {\n                return if (uri.host == HOST) {\n                    val uid = uri.getQueryParameter(\"uid\")?.toLongOrNull() ?: return null\n                    val username = uri.getQueryParameter(\"username\") ?: return null\n                    val avatar = uri.getQueryParameter(\"avatar\") ?: return null\n                    val auth = uri.getQueryParameter(\"auth\") ?: return null\n                    QrToken(uid, username, avatar, auth)\n                } else {\n                    null\n                }\n            }\n        }\n\n        override fun buildUri(): String {\n            val uri = Uri.Builder()\n                .scheme(SCHEME)\n                .authority(host)\n                .appendQueryParameter(\"uid\", uid.toString())\n                .appendQueryParameter(\"username\", username)\n                .appendQueryParameter(\"avatar\", avatar)\n                .appendQueryParameter(\"auth\", auth)\n                .build()\n            return uri.toString()\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/InterfaceMode.kt",
    "content": "package dev.aaa1115910.bv.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.stringRes\n\nenum class InterfaceMode(private val strRes: Int) {\n    Auto(R.string.interface_mode_auto),\n    TV(R.string.interface_mode_tv),\n    Mobile(R.string.interface_mode_mobile);\n\n    fun getDisplayName(context: Context): String = strRes.stringRes(context)\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/NavSwitchMode.kt",
    "content": "package dev.aaa1115910.bv.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.stringRes\n\nenum class NavSwitchMode(private val strRes: Int) {\n    Auto(R.string.nav_switch_mode_auto),\n    Confirm(R.string.nav_switch_mode_confirm);\n\n    fun getDisplayName(context: Context): String = strRes.stringRes(context)\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/PlayerType.kt",
    "content": "package dev.aaa1115910.bv.entity\n\nenum class PlayerType {\n    Media3\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/ThemeType.kt",
    "content": "package dev.aaa1115910.bv.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.stringRes\n\nenum class ThemeType(private val strRes: Int) {\n    Auto(R.string.theme_type_auto),\n    Dark(R.string.theme_type_dark),\n    Light(R.string.theme_type_light);\n\n    fun getDisplayName(context: Context): String = strRes.stringRes(context)\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/carddata/SeasonCardData.kt",
    "content": "package dev.aaa1115910.bv.entity.carddata\n\nimport dev.aaa1115910.biliapi.http.entity.search.SearchMediaResult\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.resizedImageUrl\n\ndata class SeasonCardData(\n    val seasonId: Int,\n    val title: String,\n    val subTitle: String? = null,\n    val cover: String,\n    val rating: String? = null,\n    val badge: SearchMediaResult.Badge? = null,\n) {\n    companion object {\n        fun fromPgcItem(pgcItem: dev.aaa1115910.biliapi.entity.pgc.PgcItem): SeasonCardData {\n            return SeasonCardData(\n                seasonId = pgcItem.seasonId,\n                title = pgcItem.title,\n                subTitle = pgcItem.subTitle,\n                cover = pgcItem.cover.resizedImageUrl(ImageSize.SeasonCoverThumbnail),\n                rating = pgcItem.rating,\n                badge = null\n            )\n        }\n\n        fun fromFollowingSeason(followingSeason: dev.aaa1115910.biliapi.entity.season.FollowingSeason): SeasonCardData {\n            return SeasonCardData(\n                seasonId = followingSeason.seasonId,\n                title = followingSeason.title,\n                cover = followingSeason.cover,\n                rating = null,\n                badge = null\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/carddata/VideoCardData.kt",
    "content": "package dev.aaa1115910.bv.entity.carddata\n\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport java.text.SimpleDateFormat\n\ndata class VideoCardData(\n    val avid: Long,\n    val title: String,\n    val cover: String,\n    val upName: String,\n    val upId: Long = 0,\n    val upFace: String = \"\",\n    val reason: String = \"\",\n    val play: Long? = null,\n    var playString: String = \"\",\n    val danmaku: Int? = null,\n    var danmakuString: String = \"\",\n    val time: Long? = null,\n    var timeString: String = \"\",\n    val jumpToSeason: Boolean = false,\n    val epId: Int? = null,\n    val seasonId: Int? = null,\n    val pubTime: String? = null,\n    val historyBusiness: String? = null,\n    val historyKid: Long? = null,\n    // var pubTimeString: String = \"\",\n    val isChargingArc: Boolean = false,\n    val badgeText: String = \"\"\n) {\n    init {\n        play?.let {\n            playString = if (it < 0) \"\" else if (it >= 10000) \"${it / 10000}万\" else \"$it\"\n        }\n        danmaku?.let {\n            danmakuString = if (it < 0) \"\" else if (it >= 10000) \"${it / 10000}万\" else \"$it\"\n        }\n        time?.let {\n            timeString = if (it > 0) it.formatHourMinSec() else \"\"\n        }\n        // pubTime?.let {\n        //     pubTimeString =\n        //         if (it > 0) SimpleDateFormat(\"yyyy-MM-dd\").format(java.util.Date(it * 1000L)) else \"\"\n        // }\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/db/SearchHistoryDB.kt",
    "content": "package dev.aaa1115910.bv.entity.db\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport java.util.Date\n\n@Entity(tableName = \"search_history\")\ndata class SearchHistoryDB(\n    @PrimaryKey(autoGenerate = true) val id: Int? = null,\n    @ColumnInfo(name = \"keyword\") val keyword: String,\n    @ColumnInfo(name = \"search_date\") var searchDate: Date = Date(),\n)\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/db/UserDB.kt",
    "content": "package dev.aaa1115910.bv.entity.db\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"user\")\n\ndata class UserDB(\n    @PrimaryKey(autoGenerate = true) val id: Int? = null,\n    @ColumnInfo(name = \"uid\") val uid: Long,\n    @ColumnInfo(name = \"username\") var username: String,\n    @ColumnInfo(name = \"avatar\") var avatar: String,\n    @ColumnInfo(name = \"auth\") var auth: String,\n    @ColumnInfo(name = \"lock\", defaultValue = \"\") var lock: String = \"\",\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/entity/proxy/ProxyArea.kt",
    "content": "package dev.aaa1115910.bv.entity.proxy\n\nimport dev.aaa1115910.bv.util.Prefs\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\nenum class ProxyArea {\n    MainLand, HongKong, TaiWan;\n\n    companion object {\n        private val logger = KotlinLogging.logger { }\n        fun checkProxyArea(title: String): ProxyArea {\n            val enableProxy = Prefs.enableProxy\n            val proxyArea = when {\n                !enableProxy -> MainLand\n                title.contains(Regex(\"僅.*港\")) -> HongKong\n                title.contains(Regex(\"僅.*台\")) -> TaiWan\n                else -> MainLand\n            }\n            if (enableProxy) logger.debug { \"Check proxy area: $title->$proxyArea\" }\n            return proxyArea\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/network/GithubApi.kt",
    "content": "package dev.aaa1115910.bv.network\n\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.network.entity.Release\nimport dev.aaa1115910.bv.util.NetworkUtil\nimport dev.aaa1115910.bv.util.Prefs\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.ktor.client.HttpClient\nimport io.ktor.client.content.ProgressListener\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.UserAgent\nimport io.ktor.client.plugins.compression.ContentEncoding\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.plugins.onDownload\nimport io.ktor.client.request.get\nimport io.ktor.client.request.parameter\nimport io.ktor.client.request.prepareRequest\nimport io.ktor.client.request.url\nimport io.ktor.client.statement.bodyAsChannel\nimport io.ktor.client.statement.bodyAsText\nimport io.ktor.http.URLProtocol\nimport io.ktor.serialization.kotlinx.json.json\nimport io.ktor.util.cio.writeChannel\nimport io.ktor.utils.io.copyAndClose\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.jsonObject\nimport kotlinx.serialization.json.jsonPrimitive\nimport okhttp3.OkHttpClient\nimport java.io.File\nimport java.util.concurrent.TimeUnit\n\nobject GithubApi {\n    private var endPoint = \"api.github.com\"\n    private const val OWNER = \"fantasytyx\"\n    private const val REPO = \"bv\"\n    private const val PROXY_URL = \"https://ghfast.top/\"\n    private lateinit var client: HttpClient\n    private val json = Json {\n        coerceInputValues = true\n        ignoreUnknownKeys = true\n        prettyPrint = true\n    }\n    private val isDebug get() = BuildConfig.DEBUG\n    private val isAlpha get() = Prefs.updateAlpha\n    private val logger = KotlinLogging.logger(\"GithubApi\")\n\n    init {\n        createClient()\n    }\n\n    private fun createClient() {\n        val okHttpClient = OkHttpClient.Builder()\n            .connectTimeout(10, TimeUnit.SECONDS)\n            .readTimeout(30, TimeUnit.SECONDS)\n            .writeTimeout(30, TimeUnit.SECONDS)\n            .build()\n\n        client = HttpClient(OkHttp) {\n            engine {\n                this.preconfigured = okHttpClient\n            }\n            install(UserAgent) {\n                agent = dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_WEB\n            }\n            install(ContentNegotiation) {\n                json(json)\n            }\n            install(ContentEncoding) {\n                deflate(1.0F)\n                gzip(0.9F)\n            }\n            defaultRequest {\n                url {\n                    protocol = URLProtocol.HTTPS\n                    host = endPoint\n                }\n            }\n        }\n    }\n\n    private suspend fun getReleases(\n        owner: String = OWNER,\n        repo: String = REPO,\n        pageSize: Int = 30,\n        page: Int = 1\n    ): List<Release> {\n        val response = client.get(\"repos/$owner/$repo/releases\") {\n            parameter(\"per_page\", pageSize)\n            parameter(\"page\", page)\n        }.bodyAsText()\n        checkErrorMessage(response)\n        return json.decodeFromString<List<Release>>(response)\n    }\n\n    private suspend fun getLatestRelease(\n        owner: String = OWNER,\n        repo: String = REPO\n    ): Release {\n        val response = client.get(\"repos/$owner/$repo/releases/latest\").bodyAsText()\n        checkErrorMessage(response)\n        return json.decodeFromString<Release>(response)\n    }\n\n    suspend fun getLatestPreReleaseBuild(): Release {\n        var release: Release? = null\n        var page = 1\n        while (release == null) {\n            val releases = getReleases(page = page)\n            if (releases.isEmpty()) break\n            release = releases.firstOrNull { it.isPreRelease }\n            page++\n        }\n        return release ?: throw IllegalStateException(\"No pre-release found\")\n    }\n\n    suspend fun getLatestReleaseBuild(): Release = getLatestRelease()\n\n    suspend fun getLatestBuild(): Release =\n        if (isAlpha) getLatestPreReleaseBuild() else getLatestReleaseBuild()\n\n    /**\n     * 获取比当前版本新的所有 Release 列表（按发布时间降序）\n     * - 如果当前版本在发布历史中，返回当前版本之后的所有更新\n     * - 如果当前版本不在发布历史中，只返回最新的一个版本\n     * - 如果没有更新，返回空列表\n     */\n    suspend fun getUpdateReleases(\n        currentVersionCode: Int,\n        currentVersionName: String\n    ): List<Release> {\n        val newerReleases = mutableListOf<Release>()\n        var page = 1\n        var currentVersionInHistory = false\n        val maxPages = 10\n\n        outer@ while (page <= maxPages) {\n            val releases = getReleases(page = page)\n            if (releases.isEmpty()) break\n\n            for (release in releases) {\n                if (isAlpha != release.isPreRelease) continue\n\n                val asset = release.assets.firstOrNull { it.name.startsWith(\"BV\") } ?: continue\n                val assetName = asset.name\n                val revision =\n                    runCatching { assetName.split(\"_\")[1].toInt() }.getOrNull() ?: continue\n\n                if (revision < currentVersionCode || assetName.contains(currentVersionName)) {\n                    currentVersionInHistory =\n                        revision == currentVersionCode && assetName.contains(currentVersionName)\n                    break@outer\n                }\n                newerReleases.add(release)\n            }\n            page++\n        }\n\n        // 当前版本不在发布历史中，只返回最新的一个版本\n        if (!currentVersionInHistory && newerReleases.size > 1) {\n            return listOf(newerReleases.first())\n        }\n\n        return newerReleases\n    }\n\n    private fun checkErrorMessage(data: String) {\n        val responseElement = json.parseToJsonElement(data)\n        if (responseElement !is JsonObject) return\n        val responseObject = responseElement.jsonObject\n        check(responseObject.size != 2 && responseObject[\"message\"] == null) { responseObject[\"message\"]!!.jsonPrimitive.content }\n    }\n\n    /**\n     * 下载更新文件\n     * 策略：大陆用户优先使用 ghfast.top 代理，非大陆用户优先使用直连\n     * 代理格式：https://ghfast.top/{原下载地址}\n     */\n    suspend fun downloadUpdate(\n        release: Release,\n        file: File,\n        downloadListener: ProgressListener\n    ) {\n        val downloadUrl =\n            if (isDebug) release.assets.firstOrNull { it.name.contains(\"debug\") }?.browserDownloadUrl\n            else release.assets.firstOrNull { it.name.contains(\"alpha\") || it.name.contains(\"release\") }?.browserDownloadUrl\n        downloadUrl ?: throw IllegalStateException(\"Didn't find download url\")\n        val isMainlandChina = NetworkUtil.isMainlandChina()\n\n        if (isMainlandChina) {\n            // 先尝试代理\n            val proxyUrl = PROXY_URL + downloadUrl\n            val proxyResult = runCatching {\n                downloadFile(proxyUrl, file, downloadListener)\n            }\n            if (proxyResult.isSuccess) {\n                logger.info { \"Download successful via proxy\" }\n                return\n            }\n            logger.warn(proxyResult.exceptionOrNull()) { \"Proxy download failed, trying direct connection\" }\n\n            // 代理失败，使用直连\n            try {\n                downloadFile(downloadUrl, file, downloadListener)\n                logger.info { \"Download successful via direct connection\" }\n            } catch (e: Exception) {\n                logger.error(e) { \"Direct download failed\" }\n                throw e\n            }\n        } else {\n            // 先尝试直连\n            val directResult = runCatching {\n                downloadFile(downloadUrl, file, downloadListener)\n            }\n\n            if (directResult.isSuccess) {\n                logger.info { \"Download successful via direct connection\" }\n                return\n            }\n\n            logger.warn(directResult.exceptionOrNull()) { \"Direct download failed, trying proxy\" }\n\n            // 直连失败，使用代理\n            val proxyUrl = PROXY_URL + downloadUrl\n            logger.info { \"Trying proxy: $proxyUrl\" }\n            try {\n                downloadFile(proxyUrl, file, downloadListener)\n                logger.info { \"Download successful via proxy\" }\n            } catch (e: Exception) {\n                logger.error(e) { \"Proxy download failed\" }\n                throw e\n            }\n        }\n    }\n\n    /**\n     * 执行实际的文件下载\n     */\n    private suspend fun downloadFile(\n        url: String,\n        file: File,\n        downloadListener: ProgressListener\n    ) {\n        client.prepareRequest {\n            url(url)\n            onDownload(downloadListener)\n        }.execute { response ->\n            response.bodyAsChannel().copyAndClose(file.writeChannel())\n        }\n    }\n\n    /**\n     * 测试代理连接\n     * @return 测试结果信息\n     */\n    suspend fun testProxyConnection(): String {\n        val results = StringBuilder()\n        results.appendLine(\"=== 代理连接测试 ===\")\n\n        // 1. 测试直连 GitHub API\n        results.appendLine(\"\\n1. 测试直连 GitHub API:\")\n        var release: Release? = null\n        try {\n            release = getLatestBuild()\n            results.appendLine(\"   成功! 最新版本: ${release.name}\")\n        } catch (e: Exception) {\n            results.appendLine(\"   失败: ${e.message}\")\n        }\n\n        // 2. 测试代理下载（使用实际的 APK 下载链接）\n        results.appendLine(\"\\n2. 测试代理下载 (ghfast.top):\")\n        try {\n            // 获取实际的 APK 下载链接\n            val downloadUrl = release?.assets\n                ?.firstOrNull { it.name.endsWith(\".apk\") }\n                ?.browserDownloadUrl\n            \n            if (downloadUrl == null) {\n                results.appendLine(\"   未找到 APK 下载链接\")\n            } else {\n                val proxyUrl = PROXY_URL + downloadUrl\n                results.appendLine(\"   代理链接: $proxyUrl\")\n                \n                val tempClient = HttpClient(OkHttp) {\n                    engine {\n                        val okHttp = OkHttpClient.Builder()\n                            .connectTimeout(5, TimeUnit.SECONDS)\n                            .readTimeout(10, TimeUnit.SECONDS)\n                            .build()\n                        this.preconfigured = okHttp\n                    }\n                    install(UserAgent) {\n                        agent = dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_WEB\n                    }\n                }\n                // 使用 HEAD 请求测试代理是否可用（不下载整个文件）\n                val response = tempClient.prepareRequest {\n                    url(proxyUrl)\n                    method = io.ktor.http.HttpMethod.Head\n                }.execute { it }\n                tempClient.close()\n                \n                if (response.status.value in 200..399) {\n                    results.appendLine(\"   代理可用 ✓ (状态码: ${response.status.value})\")\n                } else {\n                    results.appendLine(\"   代理响应异常 ✗ (状态码: ${response.status.value})\")\n                }\n            }\n        } catch (e: Exception) {\n            results.appendLine(\"   代理失败: ${e.message}\")\n        }\n\n        return results.toString()\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/network/HttpServer.kt",
    "content": "package dev.aaa1115910.bv.network\n\nimport dev.aaa1115910.bv.util.LogCatcherUtil\nimport io.ktor.http.ContentDisposition\nimport io.ktor.http.HttpHeaders\nimport io.ktor.http.HttpStatusCode\nimport io.ktor.server.application.Application\nimport io.ktor.server.cio.CIO\nimport io.ktor.server.cio.CIOApplicationEngine\nimport io.ktor.server.engine.EmbeddedServer\nimport io.ktor.server.engine.embeddedServer\nimport io.ktor.server.response.header\nimport io.ktor.server.response.respondFile\nimport io.ktor.server.response.respondText\nimport io.ktor.server.routing.get\nimport io.ktor.server.routing.routing\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.launch\n\nobject HttpServer {\n    var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null\n\n    @OptIn(DelicateCoroutinesApi::class)\n    fun startServer() {\n        GlobalScope.launch(Dispatchers.IO) {\n            server = embeddedServer(CIO, port = 0) {\n                homeModule()\n                logsApiModule()\n            }\n            server?.start(wait = true)\n        }\n    }\n\n    private fun Application.homeModule() {\n        routing {\n            get(\"/\") {\n                call.respondText(\"Hello World!\")\n            }\n        }\n    }\n\n    private fun Application.logsApiModule() {\n        routing {\n            get(\"/api/logs/{filename}\") {\n                val filename =\n                    call.parameters[\"filename\"] ?: return@get call.respondText(\n                        text = \"filename is null\",\n                        status = HttpStatusCode.NotFound\n                    )\n                LogCatcherUtil.updateLogFiles()\n                val file = (LogCatcherUtil.crashFiles + LogCatcherUtil.manualFiles)\n                    .find { it.name == filename } ?: return@get call.respondText(\n                    text = \"file not found\",\n                    status = HttpStatusCode.NotFound\n                )\n                call.response.header(\n                    HttpHeaders.ContentDisposition,\n                    ContentDisposition.Attachment.withParameter(\n                        ContentDisposition.Parameters.FileName,\n                        file.name\n                    ).toString()\n                )\n                call.respondFile(file)\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/network/VlcLibsApi.kt",
    "content": "package dev.aaa1115910.bv.network\n\nimport android.os.Build\nimport dev.aaa1115910.bv.network.entity.Release\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.content.ProgressListener\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.UserAgent\nimport io.ktor.client.plugins.compression.ContentEncoding\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.plugins.onDownload\nimport io.ktor.client.request.get\nimport io.ktor.client.request.prepareRequest\nimport io.ktor.client.request.url\nimport io.ktor.client.statement.bodyAsChannel\nimport io.ktor.http.URLProtocol\nimport io.ktor.serialization.kotlinx.json.json\nimport io.ktor.util.cio.writeChannel\nimport io.ktor.utils.io.copyAndClose\nimport kotlinx.serialization.json.Json\nimport java.io.File\n\nobject VlcLibsApi {\n    private var endPoint = \"api.github.com\"\n    private lateinit var client: HttpClient\n\n    init {\n        createClient()\n    }\n\n    private fun createClient() {\n        client = HttpClient(OkHttp) {\n            install(UserAgent) {\n                agent = dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_WEB\n            }\n            install(ContentNegotiation) {\n                json(Json {\n                    coerceInputValues = true\n                    ignoreUnknownKeys = true\n                    prettyPrint = true\n                })\n            }\n            install(ContentEncoding) {\n                deflate(1.0F)\n                gzip(0.9F)\n            }\n            defaultRequest {\n                url {\n                    protocol = URLProtocol.HTTPS\n                    host = endPoint\n                }\n            }\n        }\n    }\n\n    suspend fun getReleases(): List<Release> {\n        val result = mutableListOf<Release>()\n\n        runCatching {\n            result.addAll(\n                client.get(\"/repos/aaa1115910/bv-libs/releases\").body<List<Release>>()\n            )\n        }\n\n        return result\n    }\n\n    suspend fun getRelease(vlcVersion: String): Release? {\n        return getReleases().firstOrNull { it.tagName == \"libvlc-${vlcVersion}\" }\n    }\n\n    suspend fun downloadFile(\n        releaseItem: Release,\n        file: File,\n        downloadListener: ProgressListener\n    ) {\n        val fileName = getFileName()\n        if (fileName == \"\") throw IllegalStateException(\"Not supported abi\")\n\n        val downloadUrl = releaseItem.assets\n            .firstOrNull { it.name == fileName }?.browserDownloadUrl\n            ?: throw IllegalStateException(\"Not found download url\")\n        client.prepareRequest {\n            url(downloadUrl)\n            onDownload(downloadListener)\n        }.execute { response ->\n            response.bodyAsChannel().copyAndClose(file.writeChannel())\n        }\n    }\n\n    private fun getFileName(): String {\n        return if (Build.SUPPORTED_ABIS.contains(\"x86_64\")) {\n            \"x86_64.zip\"\n        } else if (Build.SUPPORTED_ABIS.contains(\"x86\")) {\n            \"x86.zip\"\n        } else if (Build.SUPPORTED_ABIS.contains(\"arm64-v8a\")) {\n            \"arm64-v8a.zip\"\n        } else if (Build.SUPPORTED_ABIS.contains(\"armeabi-v7a\")) {\n            \"armeabi-v7a.zip\"\n        } else {\n            \"\"\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/network/entity/GithubRelease.kt",
    "content": "package dev.aaa1115910.bv.network.entity\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Release(\n    val assets: List<Asset>,\n    @SerialName(\"assets_url\")\n    val assetsUrl: String,\n    val author: User,\n    val body: String,\n    @SerialName(\"created_at\")\n    val createdAt: String,\n    val draft: Boolean,\n    @SerialName(\"html_url\")\n    val htmlUrl: String,\n    val id: Int,\n    val name: String,\n    @SerialName(\"node_id\")\n    val nodeId: String,\n    val prerelease: Boolean,\n    @SerialName(\"published_at\")\n    val publishedAt: String,\n    val reactions: Reactions? = null,\n    @SerialName(\"tag_name\")\n    val tagName: String,\n    @SerialName(\"tarball_url\")\n    val tarballUrl: String,\n    @SerialName(\"target_commitish\")\n    val targetCommitish: String,\n    @SerialName(\"upload_url\")\n    val uploadUrl: String,\n    val url: String,\n    @SerialName(\"zipball_url\")\n    val zipballUrl: String\n) {\n    val isPreRelease = prerelease\n    val isRelease = !prerelease\n\n    @Serializable\n    data class Asset(\n        @SerialName(\"browser_download_url\")\n        val browserDownloadUrl: String,\n        @SerialName(\"content_type\")\n        val contentType: String,\n        @SerialName(\"created_at\")\n        val createdAt: String,\n        @SerialName(\"download_count\")\n        val downloadCount: Int,\n        val id: Int,\n        val label: String? = null,\n        val name: String,\n        @SerialName(\"node_id\")\n        val nodeId: String,\n        val size: Int,\n        val state: String,\n        @SerialName(\"updated_at\")\n        val updatedAt: String,\n        val uploader: User,\n        val url: String\n    )\n\n    @Serializable\n    data class User(\n        @SerialName(\"avatar_url\")\n        val avatarUrl: String,\n        @SerialName(\"events_url\")\n        val eventsUrl: String,\n        @SerialName(\"followers_url\")\n        val followersUrl: String,\n        @SerialName(\"following_url\")\n        val followingUrl: String,\n        @SerialName(\"gists_url\")\n        val gistsUrl: String,\n        @SerialName(\"gravatar_id\")\n        val gravatarId: String,\n        @SerialName(\"html_url\")\n        val htmlUrl: String,\n        val id: Int,\n        val login: String,\n        @SerialName(\"node_id\")\n        val nodeId: String,\n        @SerialName(\"organizations_url\")\n        val organizationsUrl: String,\n        @SerialName(\"received_events_url\")\n        val receivedEventsUrl: String,\n        @SerialName(\"repos_url\")\n        val reposUrl: String,\n        @SerialName(\"site_admin\")\n        val siteAdmin: Boolean,\n        @SerialName(\"starred_url\")\n        val starredUrl: String,\n        @SerialName(\"subscriptions_url\")\n        val subscriptionsUrl: String,\n        val type: String,\n        val url: String\n    )\n\n    @Serializable\n    data class Reactions(\n        @SerialName(\"+1\")\n        val like: Int,\n        @SerialName(\"-1\")\n        val dislike: Int,\n        val laugh: Int,\n        val hooray: Int,\n        val confused: Int,\n        val heart: Int,\n        val rocket: Int,\n        val eyes: Int\n    )\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/DefaultSubtitle.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\n/**\n * 默认字幕语言\n */\nenum class DefaultSubtitle(val value: Int) {\n    /** 关闭 */\n    Off(0),\n    /** 中文 */\n    Chinese(1),\n    /** English */\n    English(2);\n\n    fun displayName(): String = when (this) {\n        Off -> \"关闭\"\n        Chinese -> \"中文\"\n        English -> \"English\"\n    }\n\n    companion object {\n        fun fromValue(value: Int): DefaultSubtitle = entries.find { it.value == value } ?: Off\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/NextVideoStrategy.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\n\nenum class NextVideoStrategy(val ordinalValue: Int) {\n    SingleVideo(1),\n    PartAndEpisode(2),\n    PartAndEpisodeReverse(3),\n    PreloadedVideoList(4),\n    PreloadedVideoListReverse(5),\n    RelatedVideo(6);\n\n    fun displayName(context: Context): String = when (this) {\n        SingleVideo -> \"单视频\"\n        PartAndEpisode -> \"合集/分P\"\n        PartAndEpisodeReverse -> \"合集/分P-逆序\"\n        PreloadedVideoList -> \"UGC列表\"\n        PreloadedVideoListReverse -> \"UGC列表-逆序\"\n        RelatedVideo -> \"UGC推荐-随机\"\n    }\n\n    companion object {\n        fun fromOrdinal(ordinal: Int): NextVideoStrategy = entries.find { it.ordinalValue == ordinal } ?: SingleVideo\n    }\n}\n\ndata class NextVideoStrategyConfig(\n    val strategy: NextVideoStrategy,\n    val hidden: Boolean,\n    val ordinal: Int\n)\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/PlayerDefaultStartPosition.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.player.entity.DefaultStartPosition as PlayerDefaultStartPositionType\n\n/**\n * 播放默认开始位置\n */\nenum class PlayerDefaultStartPosition(val value: Int) {\n    /** 从历史位置开始 */\n    History(0),\n    /** 从开头开始 */\n    Beginning(1);\n\n    fun displayName(context: Context): String = when (this) {\n        History -> context.getString(R.string.settings_player_default_start_position_history)\n        Beginning -> context.getString(R.string.settings_player_default_start_position_beginning)\n    }\n\n    fun toPlayerType(): PlayerDefaultStartPositionType = when (this) {\n        History -> PlayerDefaultStartPositionType.History\n        Beginning -> PlayerDefaultStartPositionType.Beginning\n    }\n\n    companion object {\n        fun fromValue(value: Int): PlayerDefaultStartPosition = entries.find { it.value == value } ?: History\n    }\n}\n\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/PlayerLoadNextAction.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.R\n\n/**\n * 播放完成后加载下一个的处理策略\n */\nenum class PlayerLoadNextAction(val value: Int) {\n    /** 什么都不做 */\n    DoNothing(0),\n    /** 播放推荐视频 */\n    PlayRecommend(1),\n    /** 播放剧集和分P的下一个 */\n    PlayNextPart(2),\n    /** 播放剧集和分P的下一个或者推荐视频（没有下一个时播放推荐视频） */\n    PlayNextPartOrRecommend(3);\n\n    fun displayName(context: Context): String = when (this) {\n        DoNothing -> context.getString(R.string.settings_player_load_next_action_do_nothing)\n        PlayRecommend -> context.getString(R.string.settings_player_load_next_action_play_recommend)\n        PlayNextPart -> context.getString(R.string.settings_player_load_next_action_play_next_part)\n        PlayNextPartOrRecommend -> context.getString(R.string.settings_player_load_next_action_play_next_part_or_recommend)\n    }\n\n    companion object {\n        fun fromValue(value: Int): PlayerLoadNextAction = entries.find { it.value == value } ?: DoNothing\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/repository/UserRepository.kt",
    "content": "package dev.aaa1115910.bv.repository\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.biliapi.repositories.AuthRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.dao.AppDatabase\nimport dev.aaa1115910.bv.entity.AuthData\nimport dev.aaa1115910.bv.entity.db.UserDB\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport org.koin.core.annotation.Single\nimport java.util.Date\n\n@Single\nclass UserRepository(\n    private val authRepository: AuthRepository,\n    private val db: AppDatabase = BVApp.getAppDatabase()\n) {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    var isLogin by mutableStateOf(Prefs.isLogin)\n    var uid by mutableLongStateOf(Prefs.uid)\n    var uidCkMd5 by mutableStateOf(Prefs.uidCkMd5)\n    var sid by mutableStateOf(Prefs.sid)\n    var sessData by mutableStateOf(Prefs.sessData)\n    var biliJct by mutableStateOf(Prefs.biliJct)\n    var expiredDate by mutableStateOf(Prefs.tokenExpiredData)\n\n    var accessToken by mutableStateOf(Prefs.accessToken)\n    var refreshToken by mutableStateOf(Prefs.refreshToken)\n\n    var username by mutableStateOf(\"\")\n    var avatar by mutableStateOf(\"\")\n\n    private fun reloadFromPrefs() {\n        logger.info { \"Reload auth data from prefs\" }\n\n        uid = Prefs.uid\n        uidCkMd5 = Prefs.uidCkMd5\n        sid = Prefs.sid\n        sessData = Prefs.sessData\n        biliJct = Prefs.biliJct\n        isLogin = Prefs.isLogin\n        expiredDate = Prefs.tokenExpiredData\n        accessToken = Prefs.accessToken\n        refreshToken = Prefs.refreshToken\n    }\n\n    private fun saveToPrefs(authData: AuthData) {\n        logger.info { \"Save auth data to prefs\" }\n\n        Prefs.uid = authData.uid\n        Prefs.uidCkMd5 = authData.uidCkMd5\n        Prefs.sid = authData.sid\n        Prefs.sessData = authData.sessData\n        Prefs.biliJct = authData.biliJct\n        Prefs.isLogin = true\n        Prefs.tokenExpiredData = Date(authData.tokenExpiredData)\n        Prefs.accessToken = authData.accessToken\n        Prefs.refreshToken = authData.refreshToken\n\n        updateAuthRepository()\n    }\n\n    private fun saveToPrefs() {\n        logger.info { \"Save auth data to prefs\" }\n\n        Prefs.uid = uid\n        Prefs.uidCkMd5 = uidCkMd5\n        Prefs.sid = sid\n        Prefs.sessData = sessData\n        Prefs.biliJct = biliJct\n        Prefs.isLogin = isLogin\n        Prefs.tokenExpiredData = expiredDate\n        Prefs.accessToken = accessToken\n        Prefs.refreshToken = refreshToken\n\n        updateAuthRepository()\n    }\n\n    suspend fun logout() {\n        val user = db.userDao().findUserByUid(uid)\n        user?.let {\n            db.userDao().delete(it)\n            logger.info { \"Delete user $uid in user db\" }\n        } ?: let {\n            logger.info { \"Not found user $uid in user db\" }\n        }\n        clearAuth()\n    }\n\n    private fun clearAuth() {\n        logger.info { \"Clear auth data in UserRepository\" }\n        uid = 0\n        uidCkMd5 = \"\"\n        sid = \"\"\n        sessData = \"\"\n        biliJct = \"\"\n        isLogin = false\n        expiredDate = Date(0)\n        accessToken = \"\"\n        refreshToken = \"\"\n        saveToPrefs()\n    }\n\n    private fun updateAuthRepository() {\n        authRepository.sessionData = sessData\n        authRepository.biliJct = biliJct\n        authRepository.accessToken = accessToken\n        authRepository.mid = uid\n        authRepository.buvid3 = Prefs.buvid3\n    }\n\n    suspend fun setUser(user: UserDB) {\n        saveToPrefs(AuthData.fromJson(user.auth))\n        reloadFromPrefs()\n        BVApp.instance?.initRepository()\n        BVApp.instance?.initProxy()\n        updateAvatar()\n    }\n\n    suspend fun addUser(authData: AuthData) {\n        val existUser = db.userDao().findUserByUid(authData.uid)\n        existUser?.let {\n            it.auth = authData.toJson()\n            db.userDao().update(it)\n        } ?: let {\n            val newUser = UserDB(\n                uid = authData.uid,\n                username = \"User ${authData.uid}\",\n                avatar = \"https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg\",\n                auth = authData.toJson()\n            )\n            db.userDao().insert(newUser)\n        }\n        saveToPrefs(authData)\n        reloadFromPrefs()\n        BVApp.instance?.initRepository()\n        BVApp.instance?.initProxy()\n        updateAvatar()\n    }\n\n    suspend fun updateAvatar() {\n        val user = db.userDao().findUserByUid(uid)\n        user?.let {\n            runCatching {\n                val responseData =\n                    BiliHttpApi.getUserSelfInfo(sessData = Prefs.sessData).getResponseData()\n                logger.fInfo { \"Updating user name and avatar\" }\n                username = responseData.name\n                avatar = responseData.face\n                user.username = username\n                user.avatar = avatar\n                db.userDao().update(user)\n            }.onFailure {\n                logger.info {\n                    \"Update user name and avatar failed: ${it.stackTraceToString()}\"\n                }\n            }\n        }\n    }\n\n    suspend fun reloadAvatar() {\n        val user = db.userDao().findUserByUid(uid)\n        user?.let {\n            username = it.username\n            avatar = it.avatar\n        }\n    }\n\n    suspend fun findUserByUid(uid: Long): UserDB? {\n        return db.userDao().findUserByUid(uid)\n    }\n\n    suspend fun updateUser(user: UserDB){\n        db.userDao().update(user)\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/repository/VideoInfoRepository.kt",
    "content": "package dev.aaa1115910.bv.repository\n\nimport dev.aaa1115910.biliapi.entity.video.Tag\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.player.entity.VideoListItem\nimport org.koin.core.annotation.Single\n\n@Single\nclass VideoInfoRepository {\n    val videoList = mutableListOf<VideoListItem>()\n    val relatedVideos = mutableListOf<VideoCardData>()\n    val preloadedVideoList = mutableListOf<VideoCardData>()\n    var description: String = \"\"\n    var tags: List<Tag> = emptyList()\n    var lastPreloadedVideoIndex = 0\n\n    fun resolveLastPreloadedVideoIndex(avid: Long): Int {\n        val currentIndex = preloadedVideoList.indexOfFirst { it.avid == avid }\n        if (currentIndex >= 0) {\n            lastPreloadedVideoIndex = currentIndex\n        }\n        return lastPreloadedVideoIndex\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/ui/theme/Theme.kt",
    "content": "package dev.aaa1115910.bv.ui.theme\n\nimport android.app.Activity\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.LocalRippleConfiguration\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalView\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.dp\nimport androidx.core.view.WindowCompat\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Typography\nimport androidx.tv.material3.darkColorScheme\nimport androidx.tv.material3.lightColorScheme\nimport dev.aaa1115910.bv.component.FpsMonitor\nimport dev.aaa1115910.bv.entity.ThemeType\nimport dev.aaa1115910.bv.util.Prefs\n\n@Composable\nfun BVTheme(\n    darkTheme: Boolean = isSystemInDarkTheme(),\n    forceDark: Boolean = false,\n    content: @Composable () -> Unit\n) {\n    val context = LocalContext.current\n    val fontScale = LocalDensity.current.fontScale\n    val view = LocalView.current\n\n    val themeType = if (view.isInEditMode) ThemeType.Auto\n    else Prefs.themeTypeFlow.collectAsState(Prefs.themeType).value\n\n    val tvLightColorScheme = lightColorScheme()\n    val tvDarkColorScheme = darkColorScheme(\n        border = Color.White\n    )\n    val mobileLightColorScheme = androidx.compose.material3.lightColorScheme()\n    val mobileDarkColorScheme = androidx.compose.material3.darkColorScheme()\n\n    val colorSchemeTv = if (forceDark) tvDarkColorScheme else when (themeType) {\n        ThemeType.Auto -> if (darkTheme) tvDarkColorScheme else tvLightColorScheme\n        ThemeType.Dark -> tvDarkColorScheme\n        ThemeType.Light -> tvLightColorScheme\n    }\n    val colorSchemeCommon = if (forceDark) mobileDarkColorScheme else when (themeType) {\n        ThemeType.Auto -> if (darkTheme) mobileDarkColorScheme else mobileLightColorScheme\n        ThemeType.Dark -> mobileDarkColorScheme\n        ThemeType.Light -> mobileLightColorScheme\n    }\n    val typographyTv =\n        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) android6AndBelowTypographyTv else Typography()\n    val typographyCommon =\n        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) android6AndBelowTypographyCommon else androidx.compose.material3.Typography()\n\n\n\n    if (!view.isInEditMode) {\n        SideEffect {\n            val window = (view.context as Activity).window\n            window.statusBarColor = colorSchemeTv.primary.toArgb()\n            WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme\n        }\n    }\n\n    val density = if (view.isInEditMode)\n        LocalDensity.current.density\n    else\n        Prefs.densityFlow.collectAsState(context.resources.displayMetrics.widthPixels / 960f).value\n\n    val showFps by remember { mutableStateOf(if (!view.isInEditMode) Prefs.showFps else false) }\n\n    MaterialTheme(\n        colorScheme = colorSchemeTv,\n        typography = typographyTv\n    ) {\n        androidx.compose.material3.MaterialTheme(\n            colorScheme = colorSchemeCommon,\n            typography = typographyCommon\n        ) {\n            CompositionLocalProvider(\n                LocalRippleConfiguration provides null,\n                LocalDensity provides Density(density = density, fontScale = fontScale)\n            ) {\n                androidx.compose.material3.Surface(color = Color.Transparent) {\n                    Surface(\n                        shape = RoundedCornerShape(0.dp),\n                    ) {\n                        if (showFps) {\n                            FpsMonitor {\n                                content()\n                            }\n                        } else {\n                            content()\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/ui/theme/Typography.kt",
    "content": "package dev.aaa1115910.bv.ui.theme\n\nimport androidx.compose.ui.text.font.Font\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.tv.material3.Typography\nimport dev.aaa1115910.bv.R\n\nprivate val notoMathFont = Font(R.font.noto_sans_math_regular, FontWeight.Normal)\nprivate val notoFontFamily = FontFamily(notoMathFont)\n\nprivate val dummyTypography = Typography()\n\nval android6AndBelowTypographyTv = Typography(\n    displayLarge = dummyTypography.displayLarge.copy(fontFamily = notoFontFamily),\n    displayMedium = dummyTypography.displayMedium.copy(fontFamily = notoFontFamily),\n    displaySmall = dummyTypography.displaySmall.copy(fontFamily = notoFontFamily),\n    headlineLarge = dummyTypography.headlineLarge.copy(fontFamily = notoFontFamily),\n    headlineMedium = dummyTypography.headlineMedium.copy(fontFamily = notoFontFamily),\n    headlineSmall = dummyTypography.headlineSmall.copy(fontFamily = notoFontFamily),\n    titleLarge = dummyTypography.titleLarge.copy(fontFamily = notoFontFamily),\n    titleMedium = dummyTypography.titleMedium.copy(fontFamily = notoFontFamily),\n    titleSmall = dummyTypography.titleSmall.copy(fontFamily = notoFontFamily),\n    bodyLarge = dummyTypography.bodyLarge.copy(fontFamily = notoFontFamily),\n    bodyMedium = dummyTypography.bodyMedium.copy(fontFamily = notoFontFamily),\n    bodySmall = dummyTypography.bodySmall.copy(fontFamily = notoFontFamily),\n    labelLarge = dummyTypography.labelLarge.copy(fontFamily = notoFontFamily),\n    labelMedium = dummyTypography.labelMedium.copy(fontFamily = notoFontFamily),\n    labelSmall = dummyTypography.labelSmall.copy(fontFamily = notoFontFamily)\n)\n\nval android6AndBelowTypographyCommon = androidx.compose.material3.Typography(\n    displayLarge = dummyTypography.displayLarge.copy(fontFamily = notoFontFamily),\n    displayMedium = dummyTypography.displayMedium.copy(fontFamily = notoFontFamily),\n    displaySmall = dummyTypography.displaySmall.copy(fontFamily = notoFontFamily),\n    headlineLarge = dummyTypography.headlineLarge.copy(fontFamily = notoFontFamily),\n    headlineMedium = dummyTypography.headlineMedium.copy(fontFamily = notoFontFamily),\n    headlineSmall = dummyTypography.headlineSmall.copy(fontFamily = notoFontFamily),\n    titleLarge = dummyTypography.titleLarge.copy(fontFamily = notoFontFamily),\n    titleMedium = dummyTypography.titleMedium.copy(fontFamily = notoFontFamily),\n    titleSmall = dummyTypography.titleSmall.copy(fontFamily = notoFontFamily),\n    bodyLarge = dummyTypography.bodyLarge.copy(fontFamily = notoFontFamily),\n    bodyMedium = dummyTypography.bodyMedium.copy(fontFamily = notoFontFamily),\n    bodySmall = dummyTypography.bodySmall.copy(fontFamily = notoFontFamily),\n    labelLarge = dummyTypography.labelLarge.copy(fontFamily = notoFontFamily),\n    labelMedium = dummyTypography.labelMedium.copy(fontFamily = notoFontFamily),\n    labelSmall = dummyTypography.labelSmall.copy(fontFamily = notoFontFamily)\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/AbiUtil.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport dev.aaa1115910.bv.BVApp\nimport java.io.File\nimport java.util.zip.ZipEntry\nimport java.util.zip.ZipFile\n\nobject AbiUtil {\n    fun getApkSupportedAbiSet(): Set<String> {\n        val apkPath = BVApp.context.applicationInfo.sourceDir\n        val apkFile = File(apkPath)\n\n        var elementName: String\n\n        val abiSet = mutableSetOf<String>()\n        var zipFile: ZipFile? = null\n\n        runCatching {\n            zipFile = ZipFile(apkFile)\n            val entries = zipFile!!.entries()\n            var entry: ZipEntry\n\n            while (entries.hasMoreElements()) {\n                entry = entries.nextElement()\n                if (entry.isDirectory) continue\n                elementName = entry.name\n\n                if (elementName.startsWith(\"lib/\")) {\n                    elementName = elementName.removePrefix(\"lib/\")\n                    when {\n                        elementName.startsWith(\"arm64-v8a/\") -> abiSet.add(\"arm64-v8a\")\n                        elementName.startsWith(\"armeabi-v7a/\") -> abiSet.add(\"armeabi-v7a\")\n                        elementName.startsWith(\"x86_64/\") -> abiSet.add(\"x86_64\")\n                        elementName.startsWith(\"x86/\") -> abiSet.add(\"x86\")\n                    }\n                }\n            }\n        }\n\n        zipFile?.close()\n        return abiSet\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/BlacklistUtil.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.Blacklist.BlacklistNano\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.R\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.request.get\nimport java.io.File\nimport kotlin.system.exitProcess\n\nobject BlacklistUtil {\n    private const val BLACKLIST_DIR = \"blacklist\"\n    private const val BLACKLIST_FILENAME = \"blacklist.bin\"\n    private val logger = KotlinLogging.logger {}\n\n    private suspend fun downloadBlacklist(): ByteArray {\n        return HttpClient(OkHttp).get(BuildConfig.BLACKLIST_URL).body()\n    }\n\n    suspend fun updateBlacklist(context: Context = BVApp.context) {\n        logger.fInfo { \"updating blacklist\" }\n        var blacklistBinary: ByteArray? = null\n\n        // download blacklist binary\n        runCatching {\n            blacklistBinary = downloadBlacklist()\n            check(blacklistBinary != null) { \"blacklist is null\" }\n        }.onFailure {\n            logger.warn { \"download blacklist failed: ${it.stackTraceToString()}\" }\n            return\n        }\n\n        // validate blacklist binary\n        runCatching {\n            val blacklist = BlacklistNano.parseFrom(blacklistBinary!!)\n            logger.info { \"blacklist: [version=${blacklist.version}, count=${blacklist.count}]\" }\n        }.onFailure {\n            logger.warn { \"check blacklist validate failed: ${it.stackTraceToString()}\" }\n            return\n        }\n\n        // save blacklist binary\n        val blacklistDir = File(context.filesDir, BLACKLIST_DIR)\n        if (!blacklistDir.exists()) blacklistDir.mkdirs()\n        val blacklistFile = File(blacklistDir, BLACKLIST_FILENAME)\n        runCatching {\n            blacklistFile.delete()\n            blacklistFile.writeBytes(blacklistBinary!!)\n            logger.info { \"blacklist saved to ${blacklistFile.absolutePath}\" }\n        }.onFailure {\n            logger.warn { \"save blacklist failed: ${it.stackTraceToString()}\" }\n        }\n    }\n\n    private fun getBlacklistData(context: Context = BVApp.context): BlacklistNano? {\n        var data = context.resources.openRawResource(R.raw.blacklist).readBytes()\n        val blacklistFile = File(File(context.filesDir, BLACKLIST_DIR), BLACKLIST_FILENAME)\n        if (blacklistFile.exists()) {\n            data = blacklistFile.readBytes()\n        } else {\n            logger.warn { \"blacklist file not found\" }\n        }\n        return runCatching {\n            BlacklistNano.parseFrom(data)\n        }.getOrElse {\n            logger.warn { \"parse blacklist failed: ${it.stackTraceToString()}\" }\n            null\n        }\n    }\n\n    fun checkUid(uid: Long) {\n        val blacklist = getBlacklistData()\n        if (blacklist == null) {\n            logger.warn { \"blacklist is null\" }\n            return\n        }\n        val uidList = blacklist.uidsList\n        if (uidList.isEmpty()) {\n            logger.warn { \"blacklist uid list is empty\" }\n            return\n        }\n        val isBlacklisted = uidList.contains(uid)\n        if (isBlacklisted) {\n            logger.fInfo { \"Uid $uid is blacklisted\" }\n            Prefs.blacklistUser = true\n            exitProcess(0)\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/CodecUtil.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.media.MediaCodecInfo\nimport android.media.MediaCodecList\nimport android.os.Build\nimport android.util.Range\nimport androidx.annotation.RequiresApi\nimport androidx.core.util.toRange\n\nobject CodecUtil {\n    fun parseCodecs(): List<CodecInfoData> {\n        return MediaCodecList(MediaCodecList.ALL_CODECS)\n            .codecInfos.toList()\n            .mapNotNull { CodecInfoData.fromCodecInfo(it) }\n    }\n}\n\ndata class CodecInfoData(\n    val name: String,\n    val mimeType: String,\n    val type: CodecType,\n    val mode: CodecMode,\n    val media: CodecMedia,\n    //val codecProvider: CodecProvider,\n    val maxSupportedInstances: Int?,\n    val colorFormats: List<Int>,\n    val audioBitrateRange: IntRange?,\n    val videoBitrateRange: IntRange?,\n    val videoFrame: IntRange?,\n    val supportedFrameRates: List<SupportedFrameRate>,\n    val achievableFrameRates: List<SupportedFrameRate>\n) {\n    companion object {\n        fun fromCodecInfo(codecInfo: MediaCodecInfo): CodecInfoData? {\n            val supportedType = codecInfo.supportedTypes.firstOrNull() ?: return null\n            val capabilities = runCatching { codecInfo.getCapabilitiesForType(supportedType) }.getOrNull()\n                ?: return null\n            return CodecInfoData(\n                name = codecInfo.name,\n                mimeType = capabilities.mimeType,\n                type = CodecType.fromMediaCodecInfo(codecInfo),\n                mode = CodecMode.fromMediaCodecInfo(codecInfo),\n                media = CodecMedia.fromMediaCodecInfo(codecInfo),\n                maxSupportedInstances = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)\n                    capabilities.maxSupportedInstances else null,\n                colorFormats = capabilities.colorFormats.toList(),\n                audioBitrateRange = capabilities.audioCapabilities?.bitrateRange?.let { it.lower..it.upper },\n                videoBitrateRange = capabilities.videoCapabilities?.bitrateRange?.let { it.lower..it.upper },\n                videoFrame = capabilities.videoCapabilities?.supportedFrameRates?.let { it.lower..it.upper },\n                supportedFrameRates = runCatching { codecInfo.getSupportedFrameRates(supportedType) }\n                    .getOrDefault(emptyList()),\n                achievableFrameRates = runCatching {\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) codecInfo.getAchievableFrameRates(supportedType) else emptyList()\n                }.getOrDefault(emptyList())\n            )\n        }\n    }\n}\n\nenum class CodecType {\n    Encoder,\n    Decoder;\n\n    companion object {\n        fun fromMediaCodecInfo(mediaCodecInfo: MediaCodecInfo): CodecType {\n            return if (mediaCodecInfo.isEncoder) Encoder else Decoder\n        }\n\n    }\n}\n\nenum class CodecMedia {\n    Audio,\n    Video;\n\n    companion object {\n        fun fromMediaCodecInfo(mediaCodecInfo: MediaCodecInfo): CodecMedia {\n            return if (mediaCodecInfo.isAudioCodec()) Audio else Video\n        }\n    }\n}\n\nenum class CodecMode {\n    Hardware,\n    Software;\n\n    companion object {\n        fun fromMediaCodecInfo(mediaCodecInfo: MediaCodecInfo): CodecMode {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                return if (mediaCodecInfo.isSoftwareOnly) Software else Hardware\n            }\n\n            if (mediaCodecInfo.isAudioCodec()) return Software\n\n            val name = mediaCodecInfo.name\n            if (name.contains(\"omx.brcm.video\", true)\n                && name.contains(\"hw\", true)\n            ) return Hardware\n            if (name.startsWith(\"omx.marvell.video.hw\", true)) return Hardware\n            if (name.startsWith(\"omx.intel.hw_vd\", true)) return Hardware\n            if (name.startsWith(\"omx.qcom\", true) && name.endsWith(\"hw\")) return Hardware\n            if (name.startsWith(\"c2.vda.arc\", true)\n                || name.startsWith(\"arc.\")\n            ) return Hardware\n\n            return if (\n                name.startsWith(\"omx.google.\", true)\n                || name.contains(\"ffmpeg\", true)\n                || (name.startsWith(\"omx.sec.\", true) && name.contains(\".sw.\", true))\n                || name.equals(\"omx.qcom.video.decoder.hevcswvdec\", true)\n                || name.startsWith(\"c2.android.\", true)\n                || name.startsWith(\"c2.google.\", true)\n                || name.startsWith(\"omx.sprd.soft.\", true)\n                || name.startsWith(\"omx.avcodec.\", true)\n                || name.startsWith(\"omx.pv\", true)\n                || name.endsWith(\"sw\", true)\n                || name.endsWith(\"sw.dec\", true)\n                || name.endsWith(\"sw_vd\", true)\n                || (!name.startsWith(\"omx.\", true) && !name.startsWith(\"c2.\", true))\n            ) Software else Hardware\n        }\n    }\n}\n\nprivate fun MediaCodecInfo.isAudioCodec(): Boolean {\n    return supportedTypes.joinToString().contains(\"audio\")\n}\n\nprivate val resolutions = mapOf(\n    480 to 360,\n    720 to 480,\n    1280 to 720,\n    1920 to 1080,\n    2560 to 1440,\n    3840 to 2160,\n    7680 to 4320\n)\n\ndata class SupportedFrameRate(\n    val resolution: Pair<Int, Int>,\n    val frameRate: Range<Double>,\n    val unsupported: Boolean\n)\n\nprivate fun MediaCodecInfo.getSupportedFrameRates(supportedType: String): List<SupportedFrameRate> {\n    return resolutions.map { (width, height) ->\n        val frameRates = runCatching {\n            getCapabilitiesForType(supportedType).videoCapabilities?.getSupportedFrameRatesFor(width, height)\n        }.getOrNull()\n        SupportedFrameRate(\n            resolution = width to height,\n            frameRate = frameRates ?: ((0.0..0.0).toRange()),\n            unsupported = frameRates == null\n        )\n    }\n}\n\n@RequiresApi(Build.VERSION_CODES.M)\nprivate fun MediaCodecInfo.getAchievableFrameRates(supportedType: String): List<SupportedFrameRate> {\n    return resolutions.map { (width, height) ->\n        val frameRates = runCatching {\n            getCapabilitiesForType(supportedType).videoCapabilities?.getAchievableFrameRatesFor(width, height)\n        }.getOrNull()\n        SupportedFrameRate(\n            resolution = width to height,\n            frameRate = frameRates ?: ((0.0..0.0).toRange()),\n            unsupported = frameRates == null\n        )\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/CoilConfig.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport android.os.Build\nimport coil.ImageLoader\nimport coil.decode.GifDecoder\nimport coil.decode.ImageDecoderDecoder\nimport coil.decode.SvgDecoder\nimport coil.disk.DiskCache\nimport coil.memory.MemoryCache\nimport coil.request.CachePolicy\nimport kotlinx.coroutines.Dispatchers\nimport java.io.File\n\n/**\n * Coil ImageLoader 配置工具类\n * 优化多线程并发加载图片性能\n */\nobject CoilConfig {\n\n    private const val MEMORY_CACHE_PERCENT = 0.25 // 使用可用内存的 25%\n    private const val DISK_CACHE_SIZE = 512L * 1024 * 1024 // 512MB 磁盘缓存\n    private const val DISK_CACHE_DIRECTORY = \"image_cache\"\n\n    /**\n     * 创建优化后的 ImageLoader\n     *\n     * 优化点：\n     * 1. 配置多线程并发加载（使用 IO 调度器）\n     * 2. 配置内存缓存策略\n     * 3. 配置磁盘缓存策略\n     * 4. 支持 GIF 和 SVG 解码\n     * 5. 开启网络请求优化\n     */\n    fun createImageLoader(context: Context): ImageLoader {\n        return ImageLoader.Builder(context)\n            // 使用 IO 调度器进行多线程并发加载\n            .dispatcher(Dispatchers.IO)\n            // 配置内存缓存\n            .memoryCache {\n                MemoryCache.Builder(context)\n                    .maxSizePercent(MEMORY_CACHE_PERCENT)\n                    .build()\n            }\n            // 配置磁盘缓存\n            .diskCache {\n                DiskCache.Builder()\n                    .directory(File(context.cacheDir, DISK_CACHE_DIRECTORY))\n                    .maxSizeBytes(DISK_CACHE_SIZE)\n                    .build()\n            }\n            // 配置缓存策略\n            .memoryCachePolicy(CachePolicy.ENABLED)\n            .diskCachePolicy(CachePolicy.ENABLED)\n            .networkCachePolicy(CachePolicy.ENABLED)\n            // 配置图片解码器\n            .components {\n                // GIF 解码器\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n                    add(ImageDecoderDecoder.Factory())\n                } else {\n                    add(GifDecoder.Factory())\n                }\n                // SVG 解码器\n                add(SvgDecoder.Factory())\n            }\n            // 启用 crossfade 动画\n            .crossfade(true)\n            .crossfade(200)\n            // 允许使用硬件位图以提高性能\n            .allowHardware(true)\n            // 尊重请求中的 CacheControl 头\n            .respectCacheHeaders(true)\n            .build()\n    }\n}\n\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/DanmakuRateLimiter.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport dev.aaa1115910.biliapi.http.entity.live.DanmakuEvent\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\n\n/**\n * 直播弹幕流控器\n * \n * 使用滑动窗口算法限制弹幕发送速率，防止弹幕过多影响性能\n * \n * @param maxPerSecond 每秒最大弹幕数量，默认 100\n */\nclass DanmakuRateLimiter(\n    private val maxPerSecond: Int = 100\n) {\n    private val danmakuChannel = Channel<DanmakuEvent>(Channel.UNLIMITED)\n    private val timestamps = mutableListOf<Long>()\n    private val mutex = Mutex()\n    \n    /**\n     * 提交弹幕到流控器\n     * \n     * @param event 弹幕事件\n     * @return true 如果成功提交，false 如果 Channel 已关闭\n     */\n    suspend fun submitDanmaku(event: DanmakuEvent): Boolean {\n        return danmakuChannel.trySend(event).isSuccess\n    }\n    \n    /**\n     * 启动流控器\n     * \n     * @param scope 协程作用域\n     * @param onEmit 弹幕发送回调\n     */\n    fun start(scope: CoroutineScope, onEmit: (DanmakuEvent) -> Unit) {\n        scope.launch {\n            try {\n                while (isActive) {\n                    val result = danmakuChannel.receiveCatching()\n                    val event = result.getOrNull() ?: break\n                    \n                    // 使用滑动窗口检查是否可以发送\n                    val canEmit = mutex.withLock {\n                        val now = System.currentTimeMillis()\n                        \n                        // 移除超过 1 秒的时间戳\n                        timestamps.removeAll { now - it > 1000 }\n                        \n                        // 检查是否超过限制\n                        if (timestamps.size < maxPerSecond) {\n                            timestamps.add(now)\n                            true\n                        } else {\n                            false // 丢弃弹幕\n                        }\n                    }\n                    \n                    if (canEmit) {\n                        onEmit(event)\n                    }\n                }\n            } catch (e: Exception) {\n                // Channel 关闭或其他异常，优雅退出\n            }\n        }\n    }\n    \n    /**\n     * 停止流控器并清理资源\n     */\n    fun stop() {\n        danmakuChannel.close()\n        timestamps.clear()\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/DeviceUtil.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.app.UiModeManager\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport android.content.res.Configuration\nimport dev.aaa1115910.bv.BVApp\n\n/**\n * 用于检测设备类型和特性的工具类\n */\nobject DeviceUtil {\n    /**\n     * 判断当前设备是否为TV设备\n     * \n     * 使用多种方法进行判断:\n     * 1. 通过UiModeManager检查当前UI模式是否为TV\n     * 2. 检查设备是否声明支持Leanback特性\n     * 3. 检查设备屏幕是否为TV类型\n     * \n     * @return 如果是TV设备则返回true，否则返回false\n     */\n    fun isTvDevice(context: Context = BVApp.context): Boolean {\n        // 方法1: 使用UiModeManager检查当前UI模式\n        val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager\n        if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {\n            return true\n        }\n\n        // 方法2: 检查设备是否声明支持Leanback特性\n        if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {\n            return true\n        }\n\n        // 方法3: 检查设备屏幕配置\n        val configuration = context.resources.configuration\n        if (configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE) {\n            // 大屏幕设备 + 没有触摸屏 可能是TV设备\n            if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {\n                return true\n            }\n        }\n\n        return false\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/EnumExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonStatus\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonType\nimport dev.aaa1115910.bv.R\n\nfun FollowingSeasonStatus.getDisplayName(context: Context) = when (this) {\n    FollowingSeasonStatus.All -> context.getString(R.string.following_season_status_all)\n    FollowingSeasonStatus.Want -> context.getString(R.string.following_season_status_want)\n    FollowingSeasonStatus.Watching -> context.getString(R.string.following_season_status_watching)\n    FollowingSeasonStatus.Watched -> context.getString(R.string.following_season_status_watched)\n}\n\nfun FollowingSeasonType.getDisplayName(context: Context) = when (this) {\n    FollowingSeasonType.Bangumi -> context.getString(R.string.following_season_type_bangumi)\n    FollowingSeasonType.Cinema -> context.getString(R.string.following_season_type_film_and_television)\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/Extends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.runtime.snapshots.SnapshotStateList\nimport androidx.compose.runtime.snapshots.SnapshotStateMap\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEvent\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.type\nimport androidx.core.text.HtmlCompat\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.R\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.withContext\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.concurrent.TimeUnit\n\n/**\n * 高效更新列表，在主线程上下文执行\n * 使用差分更新算法，减少内存分配和GC压力\n * @param newList 新列表内容\n */\nsuspend fun <T> SnapshotStateList<T>.swapListWithMainContext(newList: List<T>) =\n    withContext(Dispatchers.Main) {\n        // 使用差分更新算法替代简单的clear+addAll\n        if (this@swapListWithMainContext.isEmpty()) {\n            // 当前列表为空，直接添加所有元素\n            addAll(newList)\n        } else if (newList.isEmpty()) {\n            // 新列表为空，清空当前列表\n            clear()\n        } else {\n            // 复用原有对象，只更新必要的部分\n            val currentSize = size\n            val newSize = newList.size\n            val commonSize = minOf(currentSize, newSize)\n\n            // 1. 更新共同部分，复用已有对象\n            for (i in 0 until commonSize) {\n                this@swapListWithMainContext[i] = newList[i]\n            }\n\n            // 2. 处理大小差异\n            if (newSize > currentSize) {\n                // 添加新元素\n                addAll(newList.subList(currentSize, newSize))\n            } else if (currentSize > newSize) {\n                // 移除多余元素\n                repeat(currentSize - newSize) {\n                    removeAt(newSize)\n                }\n            }\n        }\n    }\n\n/**\n * 在主线程上下文执行列表更新，并且支持延迟后回调\n * 使用优化的列表更新算法\n */\nsuspend fun <T> SnapshotStateList<T>.swapListWithMainContext(\n    newList: List<T>,\n    delay: Long,\n    afterSwap: () -> Unit\n) {\n    this@swapListWithMainContext.swapListWithMainContext(newList)\n    delay(delay)\n    afterSwap()\n}\n\n/**\n * 高效批量添加列表元素，在主线程上下文执行\n * 使用分批处理减少UI阻塞和GC压力\n */\nsuspend fun <T> SnapshotStateList<T>.addAllWithMainContext(newList: List<T>) =\n    withContext(Dispatchers.Main) {\n        if (newList.isEmpty()) return@withContext\n\n        // 如果列表过大，分批添加以减少UI阻塞\n        if (newList.size > 100) {\n            newList.chunked(50).forEach { chunk ->\n                addAll(chunk)\n                delay(10) // 给UI线程呼吸的时间\n            }\n        } else {\n            addAll(newList)\n        }\n    }\n\n/**\n * 高效批量添加列表元素，接受延迟块参数\n * 使用已优化的addAllWithMainContext方法实现\n */\nsuspend fun <T> SnapshotStateList<T>.addAllWithMainContext(newListBlock: suspend () -> List<T>) {\n    val newList = newListBlock()\n    addAllWithMainContext(newList) // 使用优化版本的批量添加\n}\n\nsuspend fun <T> SnapshotStateList<T>.addWithMainContext(item: T) =\n    withContext(Dispatchers.Main) { add(item) }\n\n\nfun <K, V> SnapshotStateMap<K, V>.swapMap(newMap: Map<K, V>) {\n    clear()\n    putAll(newMap)\n}\n\nsuspend fun <K, V> SnapshotStateMap<K, V>.swapMapWithMainContext(newMap: Map<K, V>) =\n    withContext(Dispatchers.Main) { this@swapMapWithMainContext.swapMap(newMap) }\n\nfun <K, V> SnapshotStateMap<K, V>.swapMap(newMap: Map<K, V>, afterSwap: () -> Unit) {\n    this.swapMap(newMap)\n    afterSwap()\n}\n\nfun Date.formatPubTimeString(context: Context = BVApp.context): String {\n    val temp = System.currentTimeMillis() - time\n    return when {\n        temp > 1000L * 60 * 60 * 24 -> SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\").format(this)\n        temp > 1000L * 60 * 60 -> context.getString(\n            R.string.date_format_hours_age, temp / (1000 * 60 * 60)\n        )\n\n        temp > 1000L * 60 -> context.getString(\n            R.string.date_format_minutes_age, temp / (1000 * 60)\n        )\n\n        else -> context.getString(R.string.date_format_just_now)\n    }\n}\n\nfun Long.formatHourMinSec(): String {\n    return if (this < 0L) {\n        \"\"\n    } else {\n        val hours = TimeUnit.MILLISECONDS.toHours(this)\n        val minutes = TimeUnit.MILLISECONDS.toMinutes(this) - TimeUnit.HOURS.toMinutes(hours)\n        val seconds = TimeUnit.MILLISECONDS.toSeconds(this) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(this))\n\n        if (hours > 0) {\n            String.format(\"%02d:%02d:%02d\", hours, minutes, seconds)\n        } else {\n            String.format(\"%02d:%02d\", minutes, seconds)\n        }\n    }\n}\n\nfun Long.toMBString(): String = String.format(\"%.2f MB\", this / 1024f / 1024f)\n\nfun String.removeHtmlTags(): String = HtmlCompat.fromHtml(\n    this, HtmlCompat.FROM_HTML_MODE_LEGACY\n).toString()\n\nfun KeyEvent.isKeyDown(): Boolean = type == KeyEventType.KeyDown\nfun KeyEvent.isKeyUp(): Boolean = type == KeyEventType.KeyUp\nfun KeyEvent.isDpadUp(): Boolean = key == Key.DirectionUp\nfun KeyEvent.isDpadDown(): Boolean = key == Key.DirectionDown\nfun KeyEvent.isDpadLeft(): Boolean = key == Key.DirectionLeft\nfun KeyEvent.isDpadRight(): Boolean = key == Key.DirectionRight\n\nfun Int.stringRes(context: Context): String = context.getString(this)\n\nfun LazyListState.isScrolledToEnd() =\n    canScrollForward || firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0\n\nfun LazyStaggeredGridState.isScrolledToEnd() =\n    canScrollForward || firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0\n\nfun LazyStaggeredGridState.getLane() =\n    layoutInfo.visibleItemsInfo.maxOfOrNull { it.lane + 1 }\n\n@Composable\nfun LazyListState.OnBottomReached(\n    loading: Boolean = false,\n    loadMore: () -> Unit\n) {\n    val shouldLoadMore = remember {\n        derivedStateOf {\n            val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()\n                ?: return@derivedStateOf true\n\n            lastVisibleItem.index >= layoutInfo.totalItemsCount - 5\n        }\n    }\n\n    LaunchedEffect(shouldLoadMore, loading) {\n        snapshotFlow { shouldLoadMore.value }\n            .collect {\n                if (it && !loading) loadMore()\n            }\n    }\n}\n\n@Composable\nfun LazyGridState.OnBottomReached(\n    loading: Boolean = false,\n    loadMore: () -> Unit\n) {\n    val shouldLoadMore = remember {\n        derivedStateOf {\n            val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()\n                ?: return@derivedStateOf true\n\n            lastVisibleItem.index >= layoutInfo.totalItemsCount - 5\n        }\n    }\n\n    LaunchedEffect(shouldLoadMore, loading) {\n        snapshotFlow { shouldLoadMore.value }\n            .collect {\n                if (it && !loading) loadMore()\n            }\n    }\n}\n\n@Composable\nfun LazyStaggeredGridState.OnBottomReached(\n    loading: Boolean = false,\n    loadMore: () -> Unit\n) {\n    val shouldLoadMore = remember {\n        derivedStateOf {\n            val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()\n                ?: return@derivedStateOf true\n\n            lastVisibleItem.index >= layoutInfo.totalItemsCount - 5\n        }\n    }\n\n    LaunchedEffect(shouldLoadMore, loading) {\n        snapshotFlow { shouldLoadMore.value }\n            .collect {\n                if (it && !loading) loadMore()\n            }\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/LiveStreamUrlFetcher.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.widget.Toast\nimport dev.aaa1115910.biliapi.entity.live.LiveCodec\nimport dev.aaa1115910.biliapi.entity.live.LiveRoomPlayInfoResponse\nimport dev.aaa1115910.bv.player.entity.LiveCodec as AppLiveCodec\nimport dev.aaa1115910.biliapi.entity.live.LiveStream\nimport dev.aaa1115910.biliapi.repositories.LiveRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.util.Prefs\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.koin.java.KoinJavaComponent.inject\n\nobject LiveStreamUrlFetcher {\n    private val logger = KotlinLogging.logger(\"LiveStreamUrlFetcher\")\n    private val liveRepository: LiveRepository by inject(LiveRepository::class.java)\n\n    // Codec 优先级: HEVC > AV1 > AVC\n    private val codecPriority = listOf(\"hevc\", \"av1\", \"avc\")\n\n    /**\n     * 获取直播流URL\n     * @param roomId 直播间ID\n     * @param qn 画质编号，默认30000（杜比，最高值），服务端会自动降级到实际最高可用画质\n     * @param preferredCodec 首选编码格式，默认 HLS 自动选择最佳编码\n     * @return 直播播放信息，包含流URL和可用画质列表，如果未开播或获取失败则返回null\n     */\n    suspend fun fetchLiveStreamUrl(\n        roomId: Int,\n        qn: Int = 30000,\n        preferredCodec: AppLiveCodec = AppLiveCodec.HLS\n    ): LivePlayInfo? = withContext(Dispatchers.IO) {\n        try {\n            val sessData = Prefs.sessData\n            val response = liveRepository.getLiveRoomPlayInfo(roomId, qn, sessData)\n            \n            if (response.code != 0) {\n                withContext(Dispatchers.Main) {\n                    Toast.makeText(\n                        BVApp.context,\n                        \"获取直播流失败: ${response.message}\",\n                        Toast.LENGTH_SHORT\n                    ).show()\n                }\n                return@withContext null\n            }\n\n            val data = response.data\n            if (data == null) {\n                withContext(Dispatchers.Main) {\n                    Toast.makeText(\n                        BVApp.context,\n                        \"获取直播流失败: 数据为空\",\n                        Toast.LENGTH_SHORT\n                    ).show()\n                }\n                return@withContext null\n            }\n\n            // 检查直播状态\n            if (data.liveStatus != 1) {\n                withContext(Dispatchers.Main) {\n                    Toast.makeText(\n                        BVApp.context,\n                        \"主播未开播\",\n                        Toast.LENGTH_SHORT\n                    ).show()\n                }\n                return@withContext null\n            }\n\n            // 构建画质描述映射表\n            val qnDescMap = mutableMapOf<Int, String>()\n            data.playUrlInfo?.playurl?.gQnDesc?.forEach { desc ->\n                qnDescMap[desc.qn] = desc.desc\n            }\n\n            // 解析播放URL和画质信息\n            val result = parsePlayUrl(response, preferredCodec)\n            if (result == null) {\n                withContext(Dispatchers.Main) {\n                    Toast.makeText(\n                        BVApp.context,\n                        \"无法获取播放地址\",\n                        Toast.LENGTH_SHORT\n                    ).show()\n                }\n                return@withContext null\n            }\n\n            logger.info { \"Successfully fetched live stream URL for room $roomId: ${result.url}\" }\n            logger.info { \"Current qn: ${result.currentQn}, accept_qn: ${result.acceptQn}\" }\n            LivePlayInfo(\n                roomId = roomId,\n                streamUrl = result.url,\n                isLive = true,\n                currentQn = result.currentQn,\n                acceptQn = result.acceptQn,\n                qnDescMap = qnDescMap,\n                expiresAt = result.expiresAt\n            )\n        } catch (e: Exception) {\n            logger.error(e) { \"Failed to fetch live stream URL for room $roomId\" }\n            withContext(Dispatchers.Main) {\n                Toast.makeText(\n                    BVApp.context,\n                    \"获取直播流失败: ${e.message}\",\n                    Toast.LENGTH_SHORT\n                ).show()\n            }\n            null\n        }\n    }\n\n    private data class ParseResult(\n        val url: String,\n        val currentQn: Int,\n        val acceptQn: List<Int>,\n        val expiresAt: Long = 0\n    )\n\n    /**\n     * 从URL的extra参数中解析expires时间戳\n     * @param extra URL查询参数字符串，如 \"?expires=1773483819&...\"\n     * @return 过期时间的毫秒时间戳，解析失败返回0\n     */\n    fun parseExpiresFromExtra(extra: String): Long {\n        return runCatching {\n            val regex = Regex(\"\"\"[?&]expires=(\\d+)\"\"\")\n            val match = regex.find(extra)\n            match?.groupValues?.get(1)?.toLong()?.times(1000) ?: 0L\n        }.getOrElse { 0L }\n    }\n\n    /**\n     * 解析播放URL\n     * @param response 直播房间播放信息响应\n     * @param preferredCodec 首选编码格式\n     */\n    private fun parsePlayUrl(\n        response: LiveRoomPlayInfoResponse,\n        preferredCodec: AppLiveCodec\n    ): ParseResult? {\n        val streams = response.data?.playUrlInfo?.playurl?.stream ?: return null\n\n        when (preferredCodec) {\n            AppLiveCodec.HLS -> {\n                // HLS 自动选择最佳编码（HEVC > AV1 > AVC）\n                val hlsStream = streams.find { it.protocolName == \"http_hls\" }\n                if (hlsStream != null) {\n                    val result = buildUrlFromStream(hlsStream, null)\n                    if (result != null) {\n                        logger.info { \"Using HLS stream with auto codec\" }\n                        return result\n                    }\n                }\n                // 回退到 FLV\n                val flvStream = streams.find { it.protocolName == \"http_stream\" }\n                if (flvStream != null) {\n                    val result = buildUrlFromStream(flvStream, \"avc\")\n                    if (result != null) {\n                        logger.info { \"Using FLV stream (fallback)\" }\n                        return result\n                    }\n                }\n            }\n            AppLiveCodec.FLV -> {\n                // 强制使用 FLV（仅支持 AVC）\n                val flvStream = streams.find { it.protocolName == \"http_stream\" }\n                if (flvStream != null) {\n                    val result = buildUrlFromStream(flvStream, \"avc\")\n                    if (result != null) {\n                        logger.info { \"Using FLV stream\" }\n                        return result\n                    }\n                }\n                // 回退到 HLS\n                val hlsStream = streams.find { it.protocolName == \"http_hls\" }\n                if (hlsStream != null) {\n                    val result = buildUrlFromStream(hlsStream, \"avc\")\n                    if (result != null) {\n                        logger.info { \"Using HLS stream with AVC (fallback)\" }\n                        return result\n                    }\n                }\n            }\n            AppLiveCodec.AVC -> {\n                // HLS 强制 AVC\n                val hlsStream = streams.find { it.protocolName == \"http_hls\" }\n                if (hlsStream != null) {\n                    val result = buildUrlFromStream(hlsStream, \"avc\")\n                    if (result != null) {\n                        logger.info { \"Using HLS stream with AVC\" }\n                        return result\n                    }\n                }\n                // 回退到 FLV\n                val flvStream = streams.find { it.protocolName == \"http_stream\" }\n                if (flvStream != null) {\n                    val result = buildUrlFromStream(flvStream, \"avc\")\n                    if (result != null) {\n                        logger.info { \"Using FLV stream (fallback)\" }\n                        return result\n                    }\n                }\n            }\n        }\n\n        // 使用第一个可用的流作为兜底\n        for (stream in streams) {\n            val result = buildUrlFromStream(stream, null)\n            if (result != null) {\n                logger.info { \"Using fallback stream: ${stream.protocolName}\" }\n                return result\n            }\n        }\n\n        return null\n    }\n\n    /**\n     * 从编解码器列表中按优先级选择最佳编解码器 (HEVC > AV1 > AVC)\n     */\n    private fun selectBestCodec(codecs: List<LiveCodec>): LiveCodec? {\n        for (preferredCodec in codecPriority) {\n            val codec = codecs.find { it.codecName == preferredCodec }\n            if (codec != null && codec.urlInfo.isNotEmpty()) {\n                logger.debug { \"Selected codec: ${codec.codecName}\" }\n                return codec\n            }\n        }\n        // 如果没有匹配的，取第一个有效的\n        return codecs.firstOrNull { it.urlInfo.isNotEmpty() }\n    }\n\n    /**\n     * 从流信息构建URL\n     * @param stream 直播流信息\n     * @param preferredCodecName 首选编码名称，null 表示按优先级自动选择\n     */\n    private fun buildUrlFromStream(stream: LiveStream, preferredCodecName: String?): ParseResult? {\n        // 优先选择 fmp4，其次 ts，最后 flv\n        val formatOrder = listOf(\"fmp4\", \"ts\", \"flv\")\n\n        for (formatName in formatOrder) {\n            val format = stream.format.find { it.formatName == formatName }\n            if (format != null && format.codec.isNotEmpty()) {\n                val codec = if (preferredCodecName != null) {\n                    // 指定编码时，查找指定编码\n                    format.codec.find { it.codecName == preferredCodecName && it.urlInfo.isNotEmpty() }\n                } else {\n                    // 自动选择最佳编码\n                    selectBestCodec(format.codec)\n                } ?: continue\n                val urlInfo = codec.urlInfo.first()\n                val fullUrl = \"${urlInfo.host}${codec.baseUrl}${urlInfo.extra}\"\n                val expiresAt = parseExpiresFromExtra(urlInfo.extra)\n                logger.debug { \"Built URL with format $formatName, codec ${codec.codecName}: $fullUrl\" }\n                return ParseResult(\n                    url = fullUrl,\n                    currentQn = codec.currentQn,\n                    acceptQn = codec.acceptQn,\n                    expiresAt = expiresAt\n                )\n            }\n        }\n\n        // 如果没有找到特定格式，使用第一个可用的\n        for (format in stream.format) {\n            if (format.codec.isNotEmpty()) {\n                val codec = if (preferredCodecName != null) {\n                    format.codec.find { it.codecName == preferredCodecName && it.urlInfo.isNotEmpty() }\n                } else {\n                    selectBestCodec(format.codec)\n                } ?: continue\n                val urlInfo = codec.urlInfo.first()\n                val fullUrl = \"${urlInfo.host}${codec.baseUrl}${urlInfo.extra}\"\n                val expiresAt = parseExpiresFromExtra(urlInfo.extra)\n                logger.debug { \"Built URL with fallback format ${format.formatName}, codec ${codec.codecName}: $fullUrl\" }\n                return ParseResult(\n                    url = fullUrl,\n                    currentQn = codec.currentQn,\n                    acceptQn = codec.acceptQn,\n                    expiresAt = expiresAt\n                )\n            }\n        }\n\n        return null\n    }\n}\n\n/**\n * 直播播放信息\n * @param expiresAt URL过期时间戳（毫秒），0表示未知\n */\ndata class LivePlayInfo(\n    val roomId: Int,\n    val streamUrl: String,\n    val isLive: Boolean = true,\n    val currentQn: Int = 0,\n    val acceptQn: List<Int> = emptyList(),\n    val qnDescMap: Map<Int, String> = emptyMap(),\n    val expiresAt: Long = 0\n)\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/LogCatcherUtil.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport dev.aaa1115910.bv.BVApp\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport java.io.BufferedReader\nimport java.io.File\nimport java.io.InputStreamReader\nimport java.io.OutputStreamWriter\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\nobject LogCatcherUtil {\n    private val logger = KotlinLogging.logger(\"LogCatcher\")\n    const val LOG_DIR = \"crash_logs\"\n    private const val MANUAL_LOG_PREFIX = \"logs_manual\"\n    private const val CRASH_LOG_PREFIX = \"logs_crash\"\n    private const val MAX_LOG_COUNT = 10\n    var manualFiles: List<File> = emptyList()\n    var crashFiles: List<File> = emptyList()\n\n    fun installLogCatcher() {\n        runCatching {\n            Runtime.getRuntime().exec(\"logcat -c\")\n            logger.info { \"clear logcat\" }\n        }\n        val originHandler = Thread.getDefaultUncaughtExceptionHandler()\n        Thread.setDefaultUncaughtExceptionHandler { thread, exception ->\n            logger.error(exception) { \"======== UncaughtException ========\" }\n            writeLogFile(manual = false, thread = thread, exception = exception)\n            originHandler?.uncaughtException(thread, exception)\n        }\n        clearOldLogFiles()\n    }\n\n    fun logLogcat(manual: Boolean = false) {\n        writeLogFile(manual = manual)\n    }\n\n    private fun writeLogFile(\n        manual: Boolean,\n        thread: Thread? = null,\n        exception: Throwable? = null\n    ) {\n        runCatching {\n            val process = Runtime.getRuntime().exec(\"logcat -t 10000 -v threadtime\")\n            val reader = BufferedReader(InputStreamReader(process.inputStream))\n\n            val logDir = File(BVApp.context.filesDir, LOG_DIR)\n            if (!logDir.exists()) logDir.mkdir()\n\n            val logFile = File(logDir, createFilename(manual))\n            logFile.createNewFile()\n            logger.info { \"Log file: $logFile\" }\n\n            with(logFile.writer()) {\n                writeDeviceInfo()\n                writeAppInfo()\n                if (thread != null && exception != null) {\n                    writeExceptionInfo(thread, exception)\n                }\n                appendLine(\"======== Logs ========\")\n                var line: String?\n                while (reader.readLine().also { line = it } != null) {\n                    appendLine(line)\n                }\n                flush()\n                close()\n                reader.close()\n            }\n        }.onFailure {\n            logger.error(it) { \"write log to file failed\" }\n        }\n    }\n\n    private fun OutputStreamWriter.writeExceptionInfo(thread: Thread, exception: Throwable) {\n        appendLine(\"======== Exception Info ========\")\n        appendLine(\"Thread: ${thread.name} (${thread.id})\")\n        appendLine(\"Exception Type: ${exception::class.qualifiedName ?: exception.javaClass.name}\")\n        appendLine(\"Message: ${exception.message ?: \"<no message>\"}\")\n        appendLine(\"======== Stack Trace ========\")\n        appendLine(exception.stackTraceToString())\n    }\n\n    private fun OutputStreamWriter.writeDeviceInfo() {\n        val info = BVApp.context.packageManager.getPackageInfo(BVApp.context.packageName, 0)\n        appendLine(\"======== Device info ========\")\n        appendLine(\"App Version: ${info.versionName} (${info.versionCode})\")\n        appendLine(\"Android Version: ${android.os.Build.VERSION.RELEASE} (${android.os.Build.VERSION.SDK_INT})\")\n        appendLine(\"Device: ${android.os.Build.DEVICE}\")\n        appendLine(\"Model: ${android.os.Build.MODEL}\")\n        appendLine(\"Manufacturer: ${android.os.Build.MANUFACTURER}\")\n        appendLine(\"Brand: ${android.os.Build.BRAND}\")\n        appendLine(\"Product: ${android.os.Build.PRODUCT}\")\n        appendLine(\"Type: ${android.os.Build.TYPE}\")\n    }\n\n    private fun OutputStreamWriter.writeAppInfo() {\n        appendLine(\"======== App Prefs ========\")\n        appendLine(\"Login: ${Prefs.isLogin}\")\n        appendLine(\"Incognito Mode: ${Prefs.incognitoMode}\")\n        appendLine(\"Api Type: ${Prefs.apiType.name}\")\n        appendLine(\"Default Resolution: ${Prefs.defaultQuality}\")\n        appendLine(\"Default Codec: ${Prefs.defaultVideoCodec.name}\")\n        appendLine(\"Default Audio: ${Prefs.defaultAudio.name}\")\n        appendLine(\"Enabled Proxy: ${Prefs.enableProxy}\")\n    }\n\n    private fun createFilename(manual: Boolean): String {\n        var filename = \"\"\n        filename += if (manual) MANUAL_LOG_PREFIX else CRASH_LOG_PREFIX\n        val date = SimpleDateFormat(\"yyyy-MM-dd_HH:mm:ss\", Locale.getDefault()).format(Date())\n        filename += \"_$date.log\"\n        return filename\n    }\n\n    fun updateLogFiles() {\n        val files = File(BVApp.context.filesDir, LOG_DIR).listFiles()\n        manualFiles = files\n            ?.filter { it.name.startsWith(MANUAL_LOG_PREFIX) }\n            ?.sortedBy { it.lastModified() }\n            ?: emptyList()\n        crashFiles = files\n            ?.filter { it.name.startsWith(CRASH_LOG_PREFIX) }\n            ?.sortedBy { it.lastModified() }\n            ?: emptyList()\n    }\n\n    private fun clearOldLogFiles() {\n        updateLogFiles()\n\n        if (manualFiles.size > MAX_LOG_COUNT) {\n            manualFiles.take(manualFiles.size - MAX_LOG_COUNT).forEach { it.delete() }\n        }\n        if (crashFiles.size > MAX_LOG_COUNT) {\n            crashFiles.take(crashFiles.size - MAX_LOG_COUNT).forEach { it.delete() }\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/ModifierExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport androidx.compose.animation.animateColor\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.rememberInfiniteTransition\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.border\nimport androidx.compose.material3.ShapeDefaults\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.focus.FocusState\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\n\n/**\n * 获取到焦点时显示白色边框\n */\nfun Modifier.focusedBorder(\n    shape: Shape = ShapeDefaults.Large,\n    animate: Boolean = false\n): Modifier = composed {\n    val infiniteTransition = rememberInfiniteTransition(label = \"infinite border color transition\")\n    var hasFocus by remember { mutableStateOf(false) }\n\n    val animateColor by infiniteTransition.animateColor(\n        initialValue = MaterialTheme.colorScheme.border.copy(alpha = 1f),\n        targetValue = MaterialTheme.colorScheme.border.copy(alpha = 0.1f),\n        animationSpec = infiniteRepeatable(\n            animation = tween(1000, easing = LinearEasing),\n            repeatMode = RepeatMode.Reverse\n        ),\n        label = \"focused border animate color\"\n    )\n    val borderColor = if (hasFocus) {\n        if (animate) animateColor else MaterialTheme.colorScheme.border\n    } else Color.Transparent\n\n    onFocusChanged { hasFocus = it.hasFocus }\n        .border(\n            width = 3.dp,\n            color = borderColor,\n            shape = shape\n        )\n}\n\n/**\n * 在没有获取到焦点的时候缩小，以便在获取到焦点的时候“放大”\n */\nfun Modifier.focusedScale(\n    scale: Float = 0.9f\n): Modifier = composed {\n    var hasFocus by remember { mutableStateOf(false) }\n    val scaleValue by animateFloatAsState(\n        targetValue = if (hasFocus) 1f else scale,\n        label = \"focused scale\"\n    )\n\n    onFocusChanged { hasFocus = it.hasFocus }\n        .scale(scaleValue)\n}\n\n/**\n * 延迟处理焦点变化的Modifier扩展函数\n * \n * @param delayTime 延迟时间（毫秒）\n * @param action 延迟后要执行的操作\n */\nfun Modifier.onDelayFocusChanged(\n    delayTime: Long = 200L,\n    action: (FocusState) -> Unit\n) = composed {\n    val scope = rememberCoroutineScope()\n    val debouncer = rememberDebouncer<FocusState>(delayTime)\n    \n    onFocusChanged { focusState ->\n        debouncer.debounce(scope, focusState, action)\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/NetworkUtil.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.ktor.client.HttpClient\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.HttpRequestRetry\nimport io.ktor.client.request.get\nimport io.ktor.client.statement.bodyAsText\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.selects.select\nimport kotlinx.coroutines.withContext\n\nobject NetworkUtil {\n    private lateinit var client: HttpClient\n    private val locCheckUrls = listOf(\n        \"https://www.cloudflare.com/cdn-cgi/trace\",\n        \"https://1.1.1.1/cdn-cgi/trace\"\n    )\n    private val logger = KotlinLogging.logger { }\n\n    init {\n        createClient()\n    }\n\n    private fun createClient() {\n        client = HttpClient(OkHttp) {\n            install(HttpRequestRetry) {\n                retryOnException(maxRetries = 3)\n            }\n        }\n    }\n\n    suspend fun isMainlandChina() = withContext(Dispatchers.IO) {\n        val deferreds = locCheckUrls.map { locCheckUrl ->\n            async {\n                runCatching {\n                    val result = client.get(locCheckUrl).bodyAsText()\n                    logger.info { \"Network result:\\n$result\" }\n\n                    val networkCheckResult = result\n                        .lines()\n                        .filter { it.isNotBlank() }\n                        .associate { with(it.split(\"=\")) { this[0] to this[1] } }\n\n                    require(networkCheckResult[\"loc\"] != \"CN\") { \"BV doesn't support use in mainland China\" }\n                    false\n                }.getOrDefault(true)\n            }\n        }\n\n        select {\n            deferreds.forEach { deferred ->\n                deferred.onAwait { it }\n            }\n        }.also {\n            deferreds.forEach { it.cancel() }\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/NotYetImplemented.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport dev.aaa1115910.bv.BVApp\n\nfun notYetImplemented() {\n    \"Not yet implemented\".toast(BVApp.context)\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/PartitionUtil.kt",
    "content": "package dev.aaa1115910.bv.util\n\nobject PartitionUtil {\n    val partitions = listOf(\n        Partition(\n            1, \"douga\", \"动画\", listOf(\n                Partition(24, \"mad\", \"MAD·AMV\"),\n                Partition(25, \"mmd\", \"MMD·3D\"),\n                Partition(47, \"voice\", \"短片·手书·配音\"),\n                Partition(210, \"garage_kit\", \"手办·模玩\"),\n                Partition(86, \"tokusatsu\", \"特摄\"),\n                Partition(253, \"acgntalks\", \"动漫杂谈\"),\n                Partition(27, \"other\", \"综合\")\n            )\n        ),\n        Partition(\n            13, \"anime\", \"番剧\", listOf(\n                Partition(32, \"finish\", \"完结动画\"),\n                Partition(33, \"serial\", \"连载动画\"),\n                Partition(51, \"information\", \"资讯\"),\n                Partition(152, \"offical\", \"官方延伸\")\n            )\n        ),\n        Partition(\n            167, \"guochuang\", \"国创\", listOf(\n                Partition(153, \"chinese\", \"国产动画\"),\n                Partition(168, \"original\", \"国产原创相关\"),\n                Partition(169, \"puppetry\", \"布袋戏\"),\n                Partition(195, \"motioncomic\", \"动态漫·广播剧\"),\n                Partition(170, \"information\", \"资讯\")\n            )\n        ),\n        Partition(\n            3, \"music\", \"音乐\", listOf(\n                Partition(28, \"original\", \"原创音乐\"),\n                Partition(31, \"cover\", \"翻唱\"),\n                Partition(59, \"perform\", \"演奏\"),\n                Partition(30, \"vocaloid\", \"VOCALOID·UTAU\"),\n                Partition(29, \"live\", \"音乐现场\"),\n                Partition(193, \"mv\", \"MV\"),\n                Partition(243, \"commentary\", \"乐评盘点\"),\n                Partition(244, \"tutorial\", \"音乐教学\"),\n                Partition(130, \"other\", \"音乐综合\")\n            )\n        ),\n        Partition(\n            129, \"dance\", \"舞蹈\", listOf(\n                Partition(20, \"otaku\", \"宅舞\"),\n                Partition(198, \"hiphop\", \"街舞\"),\n                Partition(199, \"star\", \"明星舞蹈\"),\n                Partition(200, \"china\", \"中国舞\"),\n                Partition(154, \"three_d\", \"舞蹈综合\"),\n                Partition(156, \"demo\", \"舞蹈教程\")\n            )\n        ),\n        Partition(\n            4, \"game\", \"游戏\", listOf(\n                Partition(17, \"stand_alone\", \"单机游戏\"),\n                Partition(171, \"esports\", \"电子竞技\"),\n                Partition(172, \"mobile\", \"手机游戏\"),\n                Partition(65, \"online\", \"网络游戏\"),\n                Partition(173, \"board\", \"桌游棋牌\"),\n                Partition(121, \"gmv\", \"GMV\"),\n                Partition(136, \"music\", \"音游\"),\n                Partition(19, \"mugen\", \"Mugen\")\n            )\n        ),\n        Partition(\n            36, \"knowledge\", \"知识\", listOf(\n                Partition(201, \"science\", \"科学科普\"),\n                Partition(124, \"social_science\", \"社科·法律·心理\"),\n                Partition(228, \"humanity_history\", \"人文历史\"),\n                Partition(207, \"business\", \"财经商业\"),\n                Partition(208, \"campus\", \"校园学习\"),\n                Partition(209, \"career\", \"职业职场\"),\n                Partition(229, \"design\", \"设计·创意\"),\n                Partition(122, \"skill\", \"野生技术协会\")\n            )\n        ),\n        Partition(\n            188, \"tech\", \"科技\", listOf(\n                Partition(95, \"digital\", \"数码\"),\n                Partition(230, \"application\", \"软件应用\"),\n                Partition(231, \"computer_tech\", \"计算机技术\"),\n                Partition(232, \"industry\", \"科工机械\"),\n                Partition(233, \"diy\", \"极客DIY\")\n            )\n        ),\n        Partition(\n            234, \"sports\", \"运动\", listOf(\n                Partition(235, \"basketball\", \"篮球\"),\n                Partition(249, \"football\", \"足球\"),\n                Partition(164, \"aerobics\", \"健身\"),\n                Partition(236, \"athletic\", \"竞技体育\"),\n                Partition(237, \"culture\", \"运动文化\"),\n                Partition(238, \"comprehensive\", \"运动综合\")\n            )\n        ),\n        Partition(\n            223, \"car\", \"汽车\", listOf(\n                Partition(245, \"racing\", \"赛车\"),\n                Partition(246, \"modifiedvehicle\", \"改装玩车\"),\n                Partition(247, \"newenergyvehicle\", \"新能源车\"),\n                Partition(248, \"touringcar\", \"房车\"),\n                Partition(240, \"motorcycle\", \"摩托车\"),\n                Partition(227, \"strategy\", \"购车攻略\"),\n                Partition(176, \"life\", \"汽车生活\")\n            )\n        ),\n        Partition(\n            160, \"life\", \"生活\", listOf(\n                Partition(138, \"funny\", \"搞笑\"),\n                Partition(250, \"travel\", \"出行\"),\n                Partition(251, \"rurallife\", \"三农\"),\n                Partition(239, \"home\", \"家居房产\"),\n                Partition(161, \"handmake\", \"手工\"),\n                Partition(162, \"painting\", \"绘画\"),\n                Partition(21, \"daily\", \"日常\")\n            )\n        ),\n        Partition(\n            211, \"food\", \"美食\", listOf(\n                Partition(76, \"make\", \"美食制作\"),\n                Partition(212, \"detective\", \"美食侦探\"),\n                Partition(213, \"measurement\", \"美食测评\"),\n                Partition(214, \"rural\", \"田园美食\"),\n                Partition(215, \"record\", \"美食记录\")\n            )\n        ),\n        Partition(\n            217, \"animal\", \"动物圈\", listOf(\n                Partition(218, \"cat\", \"喵星人\"),\n                Partition(219, \"dog\", \"汪星人\"),\n                Partition(222, \"reptiles\", \"小宠异宠\"),\n                Partition(221, \"wild_animal\", \"野生动物\"),\n                Partition(220, \"second_edition\", \"动物二创\"),\n                Partition(75, \"animal_composite\", \"动物综合\")\n            )\n        ),\n        Partition(\n            119, \"kichiku\", \"鬼畜\", listOf(\n                Partition(22, \"guide\", \"鬼畜调教\"),\n                Partition(26, \"mad\", \"音MAD\"),\n                Partition(126, \"manual_vocaloid\", \"人力VOCALOID\"),\n                Partition(216, \"theatre\", \"鬼畜剧场\"),\n                Partition(127, \"course\", \"教程演示\")\n            )\n        ),\n        Partition(\n            155, \"fashion\", \"时尚\", listOf(\n                Partition(157, \"makeup\", \"美妆护肤\"),\n                Partition(252, \"cos\", \"仿妆cos\"),\n                Partition(158, \"clothing\", \"穿搭\"),\n                Partition(159, \"catwalk\", \"时尚潮流\")\n            )\n        ),\n        Partition(\n            202, \"information\", \"资讯\", listOf(\n                Partition(203, \"hotspot\", \"热点\"),\n                Partition(204, \"global\", \"环球\"),\n                Partition(205, \"social\", \"社会\"),\n                Partition(206, \"multiple\", \"综合\")\n            )\n        ),\n        Partition(\n            5, \"ent\", \"娱乐\", listOf(\n                Partition(71, \"variety\", \"综艺\"),\n                Partition(241, \"talker\", \"娱乐杂谈\"),\n                Partition(242, \"fans\", \"粉丝创作\"),\n                Partition(137, \"celebrity\", \"明星综合\")\n            )\n        ),\n        Partition(\n            181, \"cinephile\", \"影视\", listOf(\n                Partition(182, \"cinecism\", \"影视杂谈\"),\n                Partition(183, \"montage\", \"影视剪辑\"),\n                Partition(85, \"shortfilm\", \"小剧场\"),\n                Partition(184, \"trailer_info\", \"预告·资讯\")\n            )\n        ),\n        Partition(\n            177, \"documentary\", \"纪录片\", listOf(\n                Partition(37, \"history\", \"人文·历史\"),\n                Partition(178, \"science\", \"科学·探索·自然\"),\n                Partition(179, \"military\", \"军事\"),\n                Partition(180, \"travel\", \"社会·美食·旅行\")\n            )\n        ),\n        Partition(\n            23, \"movie\", \"电影\", listOf(\n                Partition(147, \"chinese\", \"华语电影\"),\n                Partition(145, \"west\", \"欧美电影\"),\n                Partition(146, \"japan\", \"日本电影\"),\n                Partition(83, \"movie\", \"其他国家\")\n            )\n        ),\n        Partition(\n            11, \"电视剧\", \"电视剧\", listOf(\n                Partition(185, \"mainland\", \"国产剧\"),\n                Partition(187, \"overseas\", \"海外剧\")\n            )\n        )\n    )\n}\n\ndata class Partition(\n    val tid: Int,\n    val code: String,\n    val strRes: String,\n    val children: List<Partition> = emptyList()\n)\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/PgcIndexParamExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.pgc.index.Area\nimport dev.aaa1115910.biliapi.entity.pgc.index.Copyright\nimport dev.aaa1115910.biliapi.entity.pgc.index.IndexOrder\nimport dev.aaa1115910.biliapi.entity.pgc.index.IndexOrderType\nimport dev.aaa1115910.biliapi.entity.pgc.index.IsFinish\nimport dev.aaa1115910.biliapi.entity.pgc.index.PgcIndexParam\nimport dev.aaa1115910.biliapi.entity.pgc.index.Producer\nimport dev.aaa1115910.biliapi.entity.pgc.index.ReleaseDate\nimport dev.aaa1115910.biliapi.entity.pgc.index.SeasonMonth\nimport dev.aaa1115910.biliapi.entity.pgc.index.SeasonStatus\nimport dev.aaa1115910.biliapi.entity.pgc.index.SeasonVersion\nimport dev.aaa1115910.biliapi.entity.pgc.index.SpokenLanguage\nimport dev.aaa1115910.biliapi.entity.pgc.index.Style\nimport dev.aaa1115910.biliapi.entity.pgc.index.Year\nimport dev.aaa1115910.bv.R\n\nfun PgcIndexParam.getDisplayName(context: Context) = when (this) {\n    is IndexOrder ->\n        fromStringArray(context, R.array.pgc_index_filter_order_name, ordinal)\n\n    is IndexOrderType ->\n        fromStringArray(context, R.array.pgc_index_filter_order_type_name, ordinal)\n\n    is SeasonVersion ->\n        fromStringArray(context, R.array.pgc_index_filter_season_version_name, ordinal)\n\n    is SpokenLanguage ->\n        fromStringArray(context, R.array.pgc_index_filter_spoken_language_name, ordinal)\n\n    is Area ->\n        fromStringArray(context, R.array.pgc_index_filter_area_name, ordinal)\n\n    is IsFinish ->\n        fromStringArray(context, R.array.pgc_index_filter_is_finish_name, ordinal)\n\n    is Copyright ->\n        fromStringArray(context, R.array.pgc_index_filter_copyright_name, ordinal)\n\n    is SeasonStatus ->\n        fromStringArray(context, R.array.pgc_index_filter_season_status_name, ordinal)\n\n    is SeasonMonth ->\n        fromStringArray(context, R.array.pgc_index_filter_season_month_name, ordinal)\n\n    is Producer ->\n        fromStringArray(context, R.array.pgc_index_filter_producer_name, ordinal)\n\n    is Year ->\n        fromStringArray(context, R.array.pgc_index_filter_year_name, ordinal)\n\n    is ReleaseDate ->\n        fromStringArray(context, R.array.pgc_index_filter_release_date_name, ordinal)\n\n    is Style ->\n        fromStringArray(context, R.array.pgc_index_filter_style_name, ordinal)\n\n    else -> \"\"\n}\n\nprivate fun fromStringArray(\n    context: Context,\n    arrayId: Int,\n    index: Int\n): String = context.resources.getStringArray(arrayId)[index]\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/PgcTypeExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.bv.R\n\nfun PgcType.getDisplayName(context: Context) = when (this) {\n    PgcType.Anime -> R.string.pgc_type_anime\n    PgcType.GuoChuang -> R.string.pgc_type_guochuang\n    PgcType.Movie -> R.string.pgc_type_movie\n    PgcType.Documentary -> R.string.pgc_type_documentary\n    PgcType.Tv -> R.string.pgc_type_tv\n    PgcType.Variety -> R.string.pgc_type_variety\n}.stringRes(context)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/Prefs.kt",
    "content": "@file:Suppress(\"SpellCheckingInspection\")\n\npackage dev.aaa1115910.bv.util\n\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.datastore.preferences.core.booleanPreferencesKey\nimport androidx.datastore.preferences.core.floatPreferencesKey\nimport androidx.datastore.preferences.core.intPreferencesKey\nimport androidx.datastore.preferences.core.longPreferencesKey\nimport androidx.datastore.preferences.core.stringPreferencesKey\nimport de.schnettler.datastore.manager.PreferenceRequest\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.http.util.generateBuvid\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.entity.InterfaceMode\nimport dev.aaa1115910.bv.entity.NavSwitchMode\nimport dev.aaa1115910.bv.entity.PlayerType\nimport dev.aaa1115910.bv.entity.ThemeType\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.DanmakuType\nimport dev.aaa1115910.bv.player.entity.LiveCodec\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.PortraitVideoFixMode\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.player.entity.DefaultSubtitle\nimport dev.aaa1115910.bv.player.entity.PlayerLoadNextAction\nimport dev.aaa1115910.bv.player.entity.PlayerDefaultStartPosition\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.transform\nimport kotlinx.coroutines.runBlocking\nimport java.util.Date\nimport java.util.UUID\nimport kotlin.math.roundToInt\n\nobject Prefs {\n    private val dsm = BVApp.dataStoreManager\n    val logger = KotlinLogging.logger { }\n\n    var isLogin: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefIsLoginRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefIsLoginKey, value) }\n\n    var uid: Long\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefUidRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefUidKey, value) }\n\n    var sid: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefSidRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefSidKey, value) }\n\n    var sessData: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefSessDataRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefSessDataKey, value) }\n\n    var biliJct: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefBiliJctRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefBiliJctKey, value) }\n\n    var uidCkMd5: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefUidCkMd5Request).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefUidCkMd5Key, value) }\n\n    var tokenExpiredData: Date\n        get() = Date(runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefTokenExpiredDateRequest).first()\n        })\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefTokenExpiredDateKey, value.time)\n        }\n\n    var defaultQuality: Resolution\n        get() = runBlocking {\n            Resolution.fromCode(dsm.getPreferenceFlow(PrefKeys.prefDefaultQualityRequest).first())\n                ?: Resolution.R1080P\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefDefaultQualityKey, value.code)\n        }\n\n    var defaultPlaySpeed: Float\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefDefaultPlaySpeedRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDefaultPlaySpeedKey, value) }\n\n    var currentPlaySpeed: Float\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefCurrentPlaySpeedRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefCurrentPlaySpeedKey, value) }\n\n    var defaultAudio: Audio\n        get() = runBlocking {\n            Audio.fromCode(dsm.getPreferenceFlow(PrefKeys.prefDefaultAudioRequest).first())\n                ?: Audio.A192K\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDefaultAudioKey, value.code) }\n\n    var defaultDanmakuSize: Int\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultDanmakuSizeRequest).first()\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDefaultDanmakuSizeKey, value) }\n\n    var defaultDanmakuScale: Float\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultDanmakuScaleRequest).first()\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDefaultDanmakuScaleKey, value) }\n\n    var defaultDanmakuTransparency: Int\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultDanmakuTransparencyRequest).first()\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefDefaultDanmakuTransparencyKey, value)\n        }\n\n    var defaultDanmakuOpacity: Float\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultDanmakuOpacityRequest).first()\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefDefaultDanmakuOpacityKey, value)\n        }\n\n    var defaultDanmakuEnabled: Boolean\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultDanmakuEnabledRequest).first()\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefDefaultDanmakuEnabledKey, value)\n        }\n\n    var defaultDanmakuTypes: List<DanmakuType>\n        get() = runBlocking {\n            val danmakuTypeIdsString =\n                dsm.getPreferenceFlow(PrefKeys.prefDefaultDanmakuTypesRequest).first()\n            if (danmakuTypeIdsString == \"\") {\n                emptyList()\n            } else {\n                danmakuTypeIdsString.split(\",\").map { DanmakuType.entries[it.toInt()] }\n            }\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(\n                PrefKeys.prefDefaultDanmakuTypesKey,\n                value.map { it.ordinal }.joinToString(\",\")\n            )\n        }\n\n    var defaultDanmakuArea: Float\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultDanmakuAreaRequest).first()\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDefaultDanmakuAreaKey, value) }\n\n    var defaultDanmakuRollingDurationFactor: Float\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultDanmakuRollingDurationFactorRequest).first()\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefDefaultDanmakuRollingDurationFactorKey, value)\n        }\n\n    var defaultVideoCodec: dev.aaa1115910.bv.player.entity.VideoCodec\n        get() = dev.aaa1115910.bv.player.entity.VideoCodec.Companion.fromCode(\n            runBlocking { dsm.getPreferenceFlow(PrefKeys.prefDefaultVideoCodecRequest).first() }\n        )\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefDefaultVideoCodecKey, value.ordinal)\n        }\n\n    var incognitoMode: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefIncognitoModeRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefIncognitoModeKey, value) }\n\n    var defaultSubtitle: DefaultSubtitle\n        get() = runBlocking {\n            val intValue = dsm.getPreferenceFlow(PrefKeys.prefDefaultSubtitleRequest).first()\n            DefaultSubtitle.fromValue(intValue)\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDefaultSubtitleKey, value.value) }\n\n    var defaultSubtitleFontSize: TextUnit\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultSubtitleFontSizeRequest).first().sp\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefDefaultSubtitleFontSizeKey, value.value.roundToInt())\n        }\n\n    var defaultSubtitleBackgroundOpacity: Float\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultSubtitleBackgroundOpacityRequest).first()\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefDefaultSubtitleBackgroundOpacityKey, value)\n        }\n\n    var defaultSubtitleBottomPadding: Dp\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultSubtitleBottomPaddingRequest).first().dp\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(\n                PrefKeys.prefDefaultSubtitleBottomPaddingKey, value.value.roundToInt()\n            )\n        }\n\n    var showFps: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefShowFpsRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefShowFpsKey, value) }\n\n    var buvid: String\n        get() = runBlocking {\n            val id = dsm.getPreferenceFlow(PrefKeys.prefBuvidRequest).first()\n            if (id != \"\") {\n                id\n            } else {\n                val randomBuvid = generateBuvid()\n                buvid = randomBuvid\n                randomBuvid\n            }\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefBuvidKey, value) }\n\n    var buvid3: String\n        get() = runBlocking {\n            var id = dsm.getPreferenceFlow(PrefKeys.prefBuvid3Request).first()\n            if(!id.contains(\"infoc\")){\n                buvid3 = \"${UUID.randomUUID()}${(0..9).random()}infoc\"\n                id = buvid3\n            }\n            if (id != \"\") {\n                id\n            } else {\n                //random buvid3\n                val randomBuvid3 = \"${UUID.randomUUID()}${(0..9).random()}infoc\"\n                buvid3 = randomBuvid3\n                randomBuvid3\n            }\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefBuvid3Key, value) }\n\n    var playerType: PlayerType\n        get() = runBlocking {\n            runCatching {\n                PlayerType.entries[dsm.getPreferenceFlow(PrefKeys.prefPlayerTypeRequest).first()]\n            }.getOrDefault(PlayerType.Media3)\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerTypeKey, value.ordinal) }\n\n    val densityFlow: Flow<Float> get() = dsm.getPreferenceFlow(PrefKeys.prefDensityRequest)\n    var density: Float\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefDensityRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDensityKey, value) }\n\n    var updateAlpha: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefAlphaRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefAlphaKey, value) }\n\n    var accessToken: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefAccessTokenRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefAccessTokenKey, value) }\n\n    var refreshToken: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefRefreshTokenRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefRefreshTokenKey, value) }\n\n    var apiType: ApiType\n        get() = runBlocking {\n            ApiType.entries[dsm.getPreferenceFlow(PrefKeys.prefApiTypeRequest).first()]\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefApiTypeKey, value.ordinal) }\n\n    var enableProxy: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefEnabelProxyRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefEnableProxyKey, value) }\n\n    var proxyHttpServer: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefProxyHttpServerRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefProxyHttpServerKey, value) }\n\n    var proxyGRPCServer: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefProxyGRPCServerRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefProxyGRPCServerKey, value) }\n\n    var lastVersionCode: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefLastVersionCodeRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefLastVersionCodeKey, value) }\n\n    var showedRemoteControllerPanelDemo: Boolean\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefShowedRemoteControllerPanelDemoRequest).first()\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefShowedRemoteControllerPanelDemoKey, value)\n        }\n\n    var preferOfficialCdn: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPreferOfficialCdnRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPreferOfficialCdn, value) }\n\n    var ipv4Only: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefIpv4OnlyRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefIpv4OnlyKey, value) }\n\n    var defaultDanmakuMask: Boolean\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefDefaultDanmakuMaskRequest).first()\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDefaultDanmakuMask, value) }\n\n    var enableFfmpegAudioRenderer: Boolean\n        get() = runBlocking {\n            dsm.getPreferenceFlow(PrefKeys.prefEnableFfmpegEndererRequest).first()\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(\n                PrefKeys.prefEnableFfmpegAudioRenderer,\n                value\n            )\n        }\n\n    var blacklistUser: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefBlacklistUserRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefBlacklistUserKey, value) }\n\n    var themeType: ThemeType\n        get() = runBlocking {\n            ThemeType.entries[dsm.getPreferenceFlow(PrefKeys.prefThemeTypeRequest).first()]\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefThemeTypeKey, value.ordinal) }\n\n    val themeTypeFlow: Flow<ThemeType>\n        get() = dsm.getPreferenceFlow(PrefKeys.prefThemeTypeRequest)\n            .transform { ordinal -> emit(ThemeType.entries[ordinal]) }\n\n    var interfaceMode: InterfaceMode\n        get() = runBlocking {\n            InterfaceMode.entries[dsm.getPreferenceFlow(PrefKeys.prefInterfaceModeRequest).first()]\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefInterfaceModeKey, value.ordinal) }\n\n    var navSwitchMode: NavSwitchMode\n        get() = runBlocking {\n            NavSwitchMode.entries[dsm.getPreferenceFlow(PrefKeys.prefNavSwitchModeRequest).first()]\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefNavSwitchModeKey, value.ordinal) }\n\n    val navSwitchModeFlow: Flow<NavSwitchMode>\n        get() = dsm.getPreferenceFlow(PrefKeys.prefNavSwitchModeRequest)\n            .transform { ordinal -> emit(NavSwitchMode.entries[ordinal]) }\n\n    var defaultPlayMode: PlayMode\n        get() = runBlocking {\n            PlayMode.entries[dsm.getPreferenceFlow(PrefKeys.prefPlayModeRequest).first()]\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayModeKey, value.ordinal) }\n\n    var defaultHomeTab: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefDefaultHomeTabRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDefaultHomeTabKey, value) }\n\n\n    var portraitVideoFixMode: PortraitVideoFixMode\n        get() = runBlocking {\n            // 读取整型枚举值\n            val intValue = dsm.getPreferenceFlow(PrefKeys.prefPortraitVideoFixModeRequest).first()\n            PortraitVideoFixMode.fromValue(intValue)\n        }\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefPortraitVideoFixModeKey, value.value)\n        }\n\n    var playerShowDebugInfo: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPlayerShowDebugInfoRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerShowDebugInfoKey, value) }\n\n    var playerLoadNextAction: PlayerLoadNextAction\n        get() = runBlocking {\n            val intValue = dsm.getPreferenceFlow(PrefKeys.prefPlayerLoadNextActionRequest).first()\n            PlayerLoadNextAction.fromValue(intValue)\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerLoadNextActionKey, value.value) }\n\n    var playerDefaultStartPosition: PlayerDefaultStartPosition\n        get() = runBlocking {\n            val intValue = dsm.getPreferenceFlow(PrefKeys.prefPlayerDefaultStartPositionRequest).first()\n            PlayerDefaultStartPosition.fromValue(intValue)\n        }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerDefaultStartPositionKey, value.value) }\n\n    var playerExitWhenAllIsPlayed: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPlayerExitWhenAllIsPlayedRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerExitWhenAllIsPlayedKey, value) }\n\n    var playerSeekForwardStep: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPlayerSeekForwardStepRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerSeekForwardStepKey, value) }\n\n    var playerSeekBackwardStep: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPlayerSeekBackwardStepRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerSeekBackwardStepKey, value) }\n\n    var playerShowBottomProgressBar: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPlayerShowBottomProgressBarRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerShowBottomProgressBarKey, value) }\n\n    var showUGCVideoInfo: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefShowUGCVideoInfoRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefShowUGCVideoInfoKey, value) }\n\n    var isLoop: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefIsLoopRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefIsLoopKey, value) }\n\n    var showDanmaku: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefShowDanmakuRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefShowDanmakuKey, value) }\n\n    var showOnlineViewerCount: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefShowOnlineViewerCountRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefShowOnlineViewerCountKey, value) }\n\n    val showOnlineViewerCountFlow: Flow<Int>\n        get() = dsm.getPreferenceFlow(PrefKeys.prefShowOnlineViewerCountRequest)\n\n    var showLiveViewerCountTip: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefShowLiveViewerCountTipRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefShowLiveViewerCountTipKey, value) }\n\n    val showLiveViewerCountTipFlow: Flow<Int>\n        get() = dsm.getPreferenceFlow(PrefKeys.prefShowLiveViewerCountTipRequest)\n\n    var defaultLiveCodec: LiveCodec\n        get() = LiveCodec.fromCode(\n            runBlocking { dsm.getPreferenceFlow(PrefKeys.prefDefaultLiveCodecRequest).first() }\n        )\n        set(value) = runBlocking {\n            dsm.editPreference(PrefKeys.prefDefaultLiveCodecKey, value.ordinal)\n        }\n\n    // 首页导航项排序和隐藏状态\n    val homeNavItemsOrderFlow: Flow<String>\n        get() = dsm.getPreferenceFlow(PrefKeys.prefHomeNavItemsOrderRequest)\n\n    var homeNavItemsOrder: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefHomeNavItemsOrderRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefHomeNavItemsOrderKey, value) }\n\n    // UGC 顶部导航项排序和隐藏状态\n    val ugcNavItemsOrderFlow: Flow<String>\n        get() = dsm.getPreferenceFlow(PrefKeys.prefUgcNavItemsOrderRequest)\n\n    var ugcNavItemsOrder: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefUgcNavItemsOrderRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefUgcNavItemsOrderKey, value) }\n\n    // PGC 顶部导航项排序和隐藏状态\n    val pgcNavItemsOrderFlow: Flow<String>\n        get() = dsm.getPreferenceFlow(PrefKeys.prefPgcNavItemsOrderRequest)\n\n    var pgcNavItemsOrder: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPgcNavItemsOrderRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPgcNavItemsOrderKey, value) }\n\n    // 直播导航项排序和隐藏状态\n    val liveNavItemsOrderFlow: Flow<String>\n        get() = dsm.getPreferenceFlow(PrefKeys.prefLiveNavItemsOrderRequest)\n\n    var liveNavItemsOrder: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefLiveNavItemsOrderRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefLiveNavItemsOrderKey, value) }\n\n    // 主导航（左侧侧栏）排序和隐藏状态\n    val drawerNavItemsOrderFlow: Flow<String>\n        get() = dsm.getPreferenceFlow(PrefKeys.prefDrawerNavItemsOrderRequest)\n\n    var drawerNavItemsOrder: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefDrawerNavItemsOrderRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDrawerNavItemsOrderKey, value) }\n\n    // 缓存的直播分区列表（供设置页使用）\n    var cachedLiveAreaGroups: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefCachedLiveAreaGroupsRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefCachedLiveAreaGroupsKey, value) }\n\n    var enableAsyncQueueing: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefEnableAsyncQueueingRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefEnableAsyncQueueing, value) }\n\n    var skipPgcIntroOutro: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefSkipPgcIntroOutroRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefSkipPgcIntroOutroKey, value) }\n\n    var playerControllerButtonsOrder: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPlayerControllerButtonsOrderRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerControllerButtonsOrderKey, value) }\n\n    var ugcVideoInfoHistoryCount: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefUgcVideoInfoHistoryCountRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefUgcVideoInfoHistoryCountKey, value) }\n\n    var videoInfoHistoryIncludeFromPlayer: Boolean\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefVideoInfoHistoryIncludeFromPlayerRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefVideoInfoHistoryIncludeFromPlayerKey, value) }\n\n    var ugcVideoPlayerHistoryCount: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefUgcVideoPlayerHistoryCountRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefUgcVideoPlayerHistoryCountKey, value) }\n\n    var defaultDanmakuFilterLevel: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefDefaultDanmakuFilterLevelRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDefaultDanmakuFilterLevelKey, value) }\n\n    var defaultLiveDanmakuFilterLevel: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefDefaultLiveDanmakuFilterLevelRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefDefaultLiveDanmakuFilterLevelKey, value) }\n\n    var playerNextVideoStrategyOrder: String\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPlayerNextVideoStrategyOrderRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerNextVideoStrategyOrderKey, value) }\n\n    // 0 = 打开菜单, 1 = 加速播放\n    var playerLongPressAction: Int\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPlayerLongPressActionRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerLongPressActionKey, value) }\n\n    var playerLongPressSpeed: Float\n        get() = runBlocking { dsm.getPreferenceFlow(PrefKeys.prefPlayerLongPressSpeedRequest).first() }\n        set(value) = runBlocking { dsm.editPreference(PrefKeys.prefPlayerLongPressSpeedKey, value) }\n}\n\nobject PrefKeys {\n    val prefIsLoginKey = booleanPreferencesKey(\"il\")\n    val prefUidKey = longPreferencesKey(\"uid\")\n    val prefSidKey = stringPreferencesKey(\"sid\")\n    val prefSessDataKey = stringPreferencesKey(\"sd\")\n    val prefBiliJctKey = stringPreferencesKey(\"bj\")\n    val prefUidCkMd5Key = stringPreferencesKey(\"ucm\")\n    val prefTokenExpiredDateKey = longPreferencesKey(\"ted\")\n    val prefDefaultQualityKey = intPreferencesKey(\"dq\")\n    val prefDefaultAudioKey = intPreferencesKey(\"da\")\n    val prefDefaultPlaySpeedKey = floatPreferencesKey(\"dps\")\n    val prefCurrentPlaySpeedKey = floatPreferencesKey(\"cps\")\n    val prefDefaultDanmakuSizeKey = intPreferencesKey(\"dds\")\n    val prefDefaultDanmakuScaleKey = floatPreferencesKey(\"dds2\")\n    val prefDefaultDanmakuTransparencyKey = intPreferencesKey(\"ddt\")\n    val prefDefaultDanmakuOpacityKey = floatPreferencesKey(\"ddo\")\n    val prefDefaultDanmakuEnabledKey = booleanPreferencesKey(\"dde\")\n    val prefDefaultDanmakuTypesKey = stringPreferencesKey(\"ddts\")\n    val prefDefaultDanmakuAreaKey = floatPreferencesKey(\"dda\")\n    val prefDefaultDanmakuRollingDurationFactorKey = floatPreferencesKey(\"ddrdf\")\n    val prefDefaultVideoCodecKey = intPreferencesKey(\"dvc\")\n    val prefIncognitoModeKey = booleanPreferencesKey(\"im\")\n    val prefDefaultSubtitleKey = intPreferencesKey(\"default_subtitle\")\n    val prefDefaultSubtitleFontSizeKey = intPreferencesKey(\"dsfs\")\n    val prefDefaultSubtitleBackgroundOpacityKey = floatPreferencesKey(\"dsbo\")\n    val prefDefaultSubtitleBottomPaddingKey = intPreferencesKey(\"dsbp\")\n    val prefShowFpsKey = booleanPreferencesKey(\"sf\")\n    val prefBuvidKey = stringPreferencesKey(\"random_buvid\")\n    val prefBuvid3Key = stringPreferencesKey(\"random_buvid3\")\n    val prefPlayerTypeKey = intPreferencesKey(\"pt\")\n    val prefDensityKey = floatPreferencesKey(\"density\")\n    val prefAlphaKey = booleanPreferencesKey(\"alpha\")\n    val prefAccessTokenKey = stringPreferencesKey(\"access_token\")\n    val prefRefreshTokenKey = stringPreferencesKey(\"refresh_token\")\n    val prefApiTypeKey = intPreferencesKey(\"api_type\")\n    val prefEnableProxyKey = booleanPreferencesKey(\"enable_proxy\")\n    val prefProxyHttpServerKey = stringPreferencesKey(\"proxy_http_server\")\n    val prefProxyGRPCServerKey = stringPreferencesKey(\"proxy_grpc_server\")\n    val prefLastVersionCodeKey = intPreferencesKey(\"last_version_code\")\n    val prefShowedRemoteControllerPanelDemoKey = booleanPreferencesKey(\"showed_rcpd\")\n    val prefPreferOfficialCdn = booleanPreferencesKey(\"prefer_official_cdn\")\n    val prefIpv4OnlyKey = booleanPreferencesKey(\"ipv4_only\")\n    val prefDefaultDanmakuMask = booleanPreferencesKey(\"prefer_enable_webmark\")\n    val prefEnableFfmpegAudioRenderer = booleanPreferencesKey(\"enable_ffmpeg_audio_renderer\")\n    val prefBlacklistUserKey = booleanPreferencesKey(\"blacklist_user\")\n    val prefThemeTypeKey = intPreferencesKey(\"theme_type\")\n    val prefInterfaceModeKey = intPreferencesKey(\"interface_mode\")\n    val prefNavSwitchModeKey = intPreferencesKey(\"nav_switch_mode\")\n    val prefPlayModeKey = intPreferencesKey(\"play_mode\")\n    val prefDefaultHomeTabKey = intPreferencesKey(\"default_home_tab\")\n    val prefPortraitVideoFixModeKey = intPreferencesKey(\"portrait_video_fix_mode\")\n    val prefPlayerShowDebugInfoKey = booleanPreferencesKey(\"player_show_debug_info\")\n    val prefPlayerExitWhenAllIsPlayedKey = booleanPreferencesKey(\"player_exit_when_all_is_played\")\n    val prefPlayerSeekForwardStepKey = intPreferencesKey(\"player_seek_forward_step\")\n    val prefPlayerSeekBackwardStepKey = intPreferencesKey(\"player_seek_backward_step\")\n    val prefPlayerShowBottomProgressBarKey = booleanPreferencesKey(\"player_show_bottom_progress_bar\")\n    val prefShowUGCVideoInfoKey = booleanPreferencesKey(\"pref_show_ugc_video_info\")\n    val prefIsLoopKey = booleanPreferencesKey(\"player_is_loop\")\n    val prefShowDanmakuKey = booleanPreferencesKey(\"player_show_danmaku\")\n    val prefPlayerLoadNextActionKey = intPreferencesKey(\"player_load_next_action\")\n    val prefPlayerDefaultStartPositionKey = intPreferencesKey(\"player_default_start_position\")\n    val prefShowOnlineViewerCountKey = intPreferencesKey(\"show_online_viewer_count\")\n    val prefShowLiveViewerCountTipKey = intPreferencesKey(\"show_live_viewer_count_tip\")\n    val prefDefaultLiveCodecKey = intPreferencesKey(\"default_live_codec\")\n    val prefHomeNavItemsOrderKey = stringPreferencesKey(\"home_nav_items_order\")\n    val prefUgcNavItemsOrderKey = stringPreferencesKey(\"ugc_nav_items_order\")\n    val prefPgcNavItemsOrderKey = stringPreferencesKey(\"pgc_nav_items_order\")\n    val prefLiveNavItemsOrderKey = stringPreferencesKey(\"live_nav_items_order\")\n    val prefDrawerNavItemsOrderKey = stringPreferencesKey(\"drawer_nav_items_order\")\n    val prefCachedLiveAreaGroupsKey = stringPreferencesKey(\"cached_live_area_groups\")\n    val prefEnableAsyncQueueing = booleanPreferencesKey(\"enable_async_queueing\")\n    val prefSkipPgcIntroOutroKey = booleanPreferencesKey(\"skip_pgc_intro_outro\")\n    val prefPlayerControllerButtonsOrderKey = stringPreferencesKey(\"player_controller_buttons_order\")\n    val prefUgcVideoInfoHistoryCountKey = intPreferencesKey(\"ugc_video_info_history_count\")\n    val prefVideoInfoHistoryIncludeFromPlayerKey = booleanPreferencesKey(\"video_info_history_include_from_player\")\n    val prefUgcVideoPlayerHistoryCountKey = intPreferencesKey(\"ugc_video_player_history_count\")\n    val prefDefaultDanmakuFilterLevelKey = intPreferencesKey(\"default_danmaku_filter_level\")\n    val prefDefaultLiveDanmakuFilterLevelKey = intPreferencesKey(\"default_live_danmaku_filter_level\")\n    val prefPlayerNextVideoStrategyOrderKey = stringPreferencesKey(\"player_next_video_strategy_order_v2\")\n    val prefPlayerLongPressActionKey = intPreferencesKey(\"player_long_press_action\")\n    val prefPlayerLongPressSpeedKey = floatPreferencesKey(\"player_long_press_speed\")\n\n\n    val prefIsLoginRequest = PreferenceRequest(prefIsLoginKey, false)\n    val prefUidRequest = PreferenceRequest(prefUidKey, 0)\n    val prefSidRequest = PreferenceRequest(prefSidKey, \"\")\n    val prefSessDataRequest = PreferenceRequest(prefSessDataKey, \"\")\n    val prefBiliJctRequest = PreferenceRequest(prefBiliJctKey, \"\")\n    val prefUidCkMd5Request = PreferenceRequest(prefUidCkMd5Key, \"\")\n    val prefTokenExpiredDateRequest = PreferenceRequest(prefTokenExpiredDateKey, 0)\n    val prefDefaultPlaySpeedRequest = PreferenceRequest(prefDefaultPlaySpeedKey, 1f)\n    val prefCurrentPlaySpeedRequest = PreferenceRequest(prefCurrentPlaySpeedKey, 1f)\n    val prefDefaultQualityRequest = PreferenceRequest(prefDefaultQualityKey, Resolution.R1080P.code)\n    val prefDefaultAudioRequest = PreferenceRequest(prefDefaultAudioKey, Audio.A192K.code)\n    val prefDefaultDanmakuSizeRequest = PreferenceRequest(prefDefaultDanmakuSizeKey, 6)\n    val prefDefaultDanmakuScaleRequest = PreferenceRequest(prefDefaultDanmakuScaleKey, 1.25f)\n    val prefDefaultDanmakuTransparencyRequest =\n        PreferenceRequest(prefDefaultDanmakuTransparencyKey, 0)\n    val prefDefaultDanmakuOpacityRequest = PreferenceRequest(prefDefaultDanmakuOpacityKey, 0.8f)\n    val prefDefaultDanmakuEnabledRequest = PreferenceRequest(prefDefaultDanmakuEnabledKey, true)\n    val prefDefaultDanmakuTypesRequest =\n        PreferenceRequest(prefDefaultDanmakuTypesKey, \"0,1,2,3\")\n    val prefDefaultDanmakuAreaRequest = PreferenceRequest(prefDefaultDanmakuAreaKey, 0.2f)\n    val prefDefaultDanmakuRollingDurationFactorRequest =\n        PreferenceRequest(prefDefaultDanmakuRollingDurationFactorKey, 1f)\n    val prefDefaultVideoCodecRequest =\n        PreferenceRequest(prefDefaultVideoCodecKey, VideoCodec.HEVC.ordinal)\n    val prefIncognitoModeRequest = PreferenceRequest(prefIncognitoModeKey, false)\n    val prefDefaultSubtitleRequest = PreferenceRequest(prefDefaultSubtitleKey, DefaultSubtitle.Off.value)\n    val prefDefaultSubtitleFontSizeRequest = PreferenceRequest(prefDefaultSubtitleFontSizeKey, 24)\n    val prefDefaultSubtitleBackgroundOpacityRequest =\n        PreferenceRequest(prefDefaultSubtitleBackgroundOpacityKey, 0.4f)\n    val prefDefaultSubtitleBottomPaddingRequest =\n        PreferenceRequest(prefDefaultSubtitleBottomPaddingKey, 12)\n    val prefShowFpsRequest = PreferenceRequest(prefShowFpsKey, false)\n    val prefBuvidRequest = PreferenceRequest(prefBuvidKey, \"\")\n    val prefBuvid3Request = PreferenceRequest(prefBuvid3Key, \"\")\n    val prefPlayerTypeRequest = PreferenceRequest(prefPlayerTypeKey, PlayerType.Media3.ordinal)\n    val prefDensityRequest =\n        PreferenceRequest(\n            prefDensityKey,\n            runCatching { BVApp.context.resources.displayMetrics.widthPixels / 960f }\n                .getOrDefault(2f)\n        )\n\n    @Suppress(\"KotlinConstantConditions\")\n    val prefAlphaRequest = PreferenceRequest(prefAlphaKey, BuildConfig.BUILD_TYPE == \"alpha\")\n    val prefAccessTokenRequest = PreferenceRequest(prefAccessTokenKey, \"\")\n    val prefRefreshTokenRequest = PreferenceRequest(prefRefreshTokenKey, \"\")\n    val prefApiTypeRequest = PreferenceRequest(prefApiTypeKey, 0)\n    val prefEnabelProxyRequest = PreferenceRequest(prefEnableProxyKey, false)\n    val prefProxyHttpServerRequest = PreferenceRequest(prefProxyHttpServerKey, \"\")\n    val prefProxyGRPCServerRequest = PreferenceRequest(prefProxyGRPCServerKey, \"\")\n    val prefLastVersionCodeRequest = PreferenceRequest(prefLastVersionCodeKey, 0)\n    val prefShowedRemoteControllerPanelDemoRequest =\n        PreferenceRequest(prefShowedRemoteControllerPanelDemoKey, false)\n    val prefPreferOfficialCdnRequest = PreferenceRequest(prefPreferOfficialCdn, false)\n    val prefIpv4OnlyRequest = PreferenceRequest(prefIpv4OnlyKey, false)\n    val prefDefaultDanmakuMaskRequest = PreferenceRequest(prefDefaultDanmakuMask, false)\n    val prefEnableFfmpegEndererRequest = PreferenceRequest(prefEnableFfmpegAudioRenderer, true)\n    val prefBlacklistUserRequest = PreferenceRequest(prefBlacklistUserKey, false)\n    val prefThemeTypeRequest = PreferenceRequest(prefThemeTypeKey, ThemeType.Auto.ordinal)\n    val prefInterfaceModeRequest = PreferenceRequest(prefInterfaceModeKey, InterfaceMode.Auto.ordinal)\n    val prefNavSwitchModeRequest = PreferenceRequest(prefNavSwitchModeKey, NavSwitchMode.Auto.ordinal)\n    val prefPlayModeRequest = PreferenceRequest(prefPlayModeKey, PlayMode.PartAndEpisode.ordinal)\n    val prefDefaultHomeTabRequest = PreferenceRequest(prefDefaultHomeTabKey, 0)\n    val prefPortraitVideoFixModeRequest = PreferenceRequest(prefPortraitVideoFixModeKey, 0)\n    val prefPlayerShowDebugInfoRequest = PreferenceRequest(prefPlayerShowDebugInfoKey, false)\n    val prefPlayerExitWhenAllIsPlayedRequest = PreferenceRequest(prefPlayerExitWhenAllIsPlayedKey, true)\n    val prefPlayerSeekForwardStepRequest = PreferenceRequest(prefPlayerSeekForwardStepKey, 10)\n    val prefPlayerSeekBackwardStepRequest = PreferenceRequest(prefPlayerSeekBackwardStepKey, 5)\n    val prefPlayerShowBottomProgressBarRequest = PreferenceRequest(prefPlayerShowBottomProgressBarKey, false)\n    val prefShowUGCVideoInfoRequest = PreferenceRequest(prefShowUGCVideoInfoKey, true)\n    val prefIsLoopRequest = PreferenceRequest(prefIsLoopKey, false)\n    val prefShowDanmakuRequest = PreferenceRequest(prefShowDanmakuKey, true)\n    val prefPlayerLoadNextActionRequest = PreferenceRequest(prefPlayerLoadNextActionKey, PlayerLoadNextAction.DoNothing.value)\n    val prefPlayerDefaultStartPositionRequest = PreferenceRequest(prefPlayerDefaultStartPositionKey, PlayerDefaultStartPosition.Beginning.value)\n    val prefShowOnlineViewerCountRequest = PreferenceRequest(prefShowOnlineViewerCountKey, 2)  // 0 = 不显示, 1 = 30 秒后隐藏, 2 = 始终显示\n    val prefShowLiveViewerCountTipRequest = PreferenceRequest(prefShowLiveViewerCountTipKey, 2)  // 0 = 不显示, 1 = 30 秒后隐藏, 2 = 始终显示\n    val prefDefaultLiveCodecRequest = PreferenceRequest(prefDefaultLiveCodecKey, LiveCodec.HLS.ordinal)\n    // 默认留空：表示按枚举原始顺序全部显示（解析侧会处理 blank）\n    val prefHomeNavItemsOrderRequest = PreferenceRequest(prefHomeNavItemsOrderKey, \"\")\n    val prefUgcNavItemsOrderRequest = PreferenceRequest(prefUgcNavItemsOrderKey, \"\")\n    val prefPgcNavItemsOrderRequest = PreferenceRequest(prefPgcNavItemsOrderKey, \"\")\n    val prefLiveNavItemsOrderRequest = PreferenceRequest(prefLiveNavItemsOrderKey, \"\")\n    val prefDrawerNavItemsOrderRequest = PreferenceRequest(prefDrawerNavItemsOrderKey, \"\")\n    val prefCachedLiveAreaGroupsRequest = PreferenceRequest(prefCachedLiveAreaGroupsKey, \"\")\n    val prefEnableAsyncQueueingRequest = PreferenceRequest(prefEnableAsyncQueueing, false)\n    val prefSkipPgcIntroOutroRequest = PreferenceRequest(prefSkipPgcIntroOutroKey, false)\n    val prefPlayerControllerButtonsOrderRequest = PreferenceRequest(prefPlayerControllerButtonsOrderKey, \"\")\n    val prefUgcVideoInfoHistoryCountRequest = PreferenceRequest(prefUgcVideoInfoHistoryCountKey, 2)\n    val prefVideoInfoHistoryIncludeFromPlayerRequest = PreferenceRequest(prefVideoInfoHistoryIncludeFromPlayerKey, true)\n    val prefUgcVideoPlayerHistoryCountRequest = PreferenceRequest(prefUgcVideoPlayerHistoryCountKey, 1)\n    val prefDefaultDanmakuFilterLevelRequest = PreferenceRequest(prefDefaultDanmakuFilterLevelKey, 0)\n    val prefDefaultLiveDanmakuFilterLevelRequest = PreferenceRequest(prefDefaultLiveDanmakuFilterLevelKey, 0)\n    val prefPlayerNextVideoStrategyOrderRequest = PreferenceRequest(\n        key = prefPlayerNextVideoStrategyOrderKey,\n        defaultValue = \"2,-3,4,-5,6\"\n    )\n    val prefPlayerLongPressActionRequest = PreferenceRequest(prefPlayerLongPressActionKey, 1)\n    val prefPlayerLongPressSpeedRequest = PreferenceRequest(prefPlayerLongPressSpeedKey, 2f)\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/UgcTypeExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.ugc.UgcType\nimport dev.aaa1115910.bv.R\n\nfun UgcType.getDisplayName(context: Context) = when (this) {\n    UgcType.Douga -> R.string.ugc_type_douga\n    UgcType.DougaMad -> R.string.ugc_type_douga_mad\n    UgcType.DougaMmd -> R.string.ugc_type_douga_mmd\n    UgcType.DougaHandDrawn -> R.string.ugc_type_douga_hand_drawn\n    UgcType.DougaVoice -> R.string.ugc_type_douga_voice\n    UgcType.DougaGarageKit -> R.string.ugc_type_douga_garage_kit\n    UgcType.DougaTokusatsu -> R.string.ugc_type_douga_tokusatsu\n    UgcType.DougaAcgnTalks -> R.string.ugc_type_douga_acgn_talks\n    UgcType.DougaOther -> R.string.ugc_type_douga_other\n\n    UgcType.Game -> R.string.ugc_type_game\n    UgcType.GameStandAlone -> R.string.ugc_type_game_stand_alone\n    UgcType.GameESports -> R.string.ugc_type_game_e_sports\n    UgcType.GameMobile -> R.string.ugc_type_game_mobile\n    UgcType.GameOnline -> R.string.ugc_type_game_online\n    UgcType.GameBoard -> R.string.ugc_type_game_board\n    UgcType.GameGmv -> R.string.ugc_type_game_gmv\n    UgcType.GameMusic -> R.string.ugc_type_game_music\n    UgcType.GameMugen -> R.string.ugc_type_game_mugen\n\n    UgcType.Kichiku -> R.string.ugc_type_kichiku\n    UgcType.KichikuGuide -> R.string.ugc_type_kichiku_guild\n    UgcType.KichikuMad -> R.string.ugc_type_kichiku_mad\n    UgcType.KichikuManualVocaloid -> R.string.ugc_type_kichiku_manual_vocaloid\n    UgcType.KichikuTheatre -> R.string.ugc_type_kichiku_theatre\n    UgcType.KichikuCourse -> R.string.ugc_type_kichiku_course\n\n    UgcType.Music -> R.string.ugc_type_music\n    UgcType.MusicOriginal -> R.string.ugc_type_music_original\n    UgcType.MusicLive -> R.string.ugc_type_music_live\n    UgcType.MusicCover -> R.string.ugc_type_music_cover\n    UgcType.MusicPerform -> R.string.ugc_type_music_perform\n    UgcType.MusicCommentary -> R.string.ugc_type_music_commentary\n    UgcType.MusicVocaloidUtau -> R.string.ugc_type_music_vocaloid_utau\n    UgcType.MusicMv -> R.string.ugc_type_music_mv\n    UgcType.MusicFanVideos -> R.string.ugc_type_music_fan_videos\n    UgcType.MusicAiMusic -> R.string.ugc_type_music_ai_music\n    UgcType.MusicRadio -> R.string.ugc_type_music_radio\n    UgcType.MusicTutorial -> R.string.ugc_type_music_tutorial\n    UgcType.MusicOther -> R.string.ugc_type_music_other\n\n    UgcType.Dance -> R.string.ugc_type_dance\n    UgcType.DanceOtaku -> R.string.ugc_type_dance_otaku\n    UgcType.DanceHiphop -> R.string.ugc_type_dance_hiphop\n    UgcType.DanceStar -> R.string.ugc_type_dance_star\n    UgcType.DanceChina -> R.string.ugc_type_dance_china\n    UgcType.DanceGestures -> R.string.ugc_type_dance_gestures\n    UgcType.DanceThreeD -> R.string.ugc_type_dance_three_d\n    UgcType.DanceDemo -> R.string.ugc_type_dance_demo\n\n    UgcType.Cinephile -> R.string.ugc_type_cinephile\n    UgcType.CinephileCinecism -> R.string.ugc_type_cinephile_cinecism\n    UgcType.CinephileNibtage -> R.string.ugc_type_cinephile_nibtage\n    UgcType.CinephileMashup -> R.string.ugc_type_cinephile_mashup\n    UgcType.CinephileAiImagine -> R.string.ugc_type_cinephile_ai_imagine\n    UgcType.CinephileTrailerInfo -> R.string.ugc_type_cinephile_trailer_info\n    UgcType.CinephileShortPlay -> R.string.ugc_type_cinephile_short_play\n    UgcType.CinephileShortFilm -> R.string.ugc_type_cinephile_short_film\n    UgcType.CinephileComperhensive -> R.string.ugc_type_cinephile_comperhensive\n\n    UgcType.Ent -> R.string.ugc_type_ent\n    UgcType.EntTalker -> R.string.ugc_type_ent_talker\n    UgcType.EntCpRecommendation -> R.string.ugc_type_ent_cp_recommendation\n    UgcType.EntBeauty -> R.string.ugc_type_ent_beauty\n    UgcType.EntFans -> R.string.ugc_type_ent_fans\n    UgcType.EntEntertainmentNews -> R.string.ugc_type_ent_entertainment_news\n    UgcType.EntCelebrity -> R.string.ugc_type_ent_celebrity\n    UgcType.EntVariety -> R.string.ugc_type_ent_variety\n\n    UgcType.Knowledge -> R.string.ugc_type_knowledge\n    UgcType.KnowledgeScience -> R.string.ugc_type_knowledge_science\n    UgcType.KnowledgeSocialScience -> R.string.ugc_type_knowledge_social_science\n    UgcType.KnowledgeHumanity -> R.string.ugc_type_knowledge_humanity\n    UgcType.KnowledgeBusiness -> R.string.ugc_type_knowledge_business\n    UgcType.KnowledgeCampus -> R.string.ugc_type_knowledge_campus\n    UgcType.KnowledgeCareer -> R.string.ugc_type_knowledge_career\n    UgcType.KnowledgeDesign -> R.string.ugc_type_knowledge_design\n    UgcType.KnowledgeSkill -> R.string.ugc_type_knowledge_skill\n\n    UgcType.Tech -> R.string.ugc_type_tech\n    UgcType.TechDigital -> R.string.ugc_type_tech_digital\n    UgcType.TechApplication -> R.string.ugc_type_tech_application\n    UgcType.TechComputerTech -> R.string.ugc_type_tech_computer_tech\n    UgcType.TechIndustry -> R.string.ugc_type_tech_industry\n    UgcType.TechDiy -> R.string.ugc_type_tech_diy\n\n    UgcType.Information -> R.string.ugc_type_information\n    UgcType.InformationHotspot -> R.string.ugc_type_information_hotspot\n    UgcType.InformationGlobal -> R.string.ugc_type_information_global\n    UgcType.InformationSocial -> R.string.ugc_type_information_social\n    UgcType.InformationMultiple -> R.string.ugc_type_information_multiple\n\n    UgcType.Food -> R.string.ugc_type_food\n    UgcType.FoodMake -> R.string.ugc_type_food_make\n    UgcType.FoodDetective -> R.string.ugc_type_food_detective\n    UgcType.FoodMeasurement -> R.string.ugc_type_food_measurement\n    UgcType.FoodRural -> R.string.ugc_type_food_rural\n    UgcType.FoodRecord -> R.string.ugc_type_food_record\n\n    UgcType.Life -> R.string.ugc_type_life\n    UgcType.LifeFunny -> R.string.ugc_type_life_funny\n    UgcType.LifeParenting -> R.string.ugc_type_life_parenting\n    UgcType.LifeTravel -> R.string.ugc_type_life_travel\n    UgcType.LiseRuralLife -> R.string.ugc_type_life_rural_life\n    UgcType.LifeHome -> R.string.ugc_type_life_home\n    UgcType.LifeHandMake -> R.string.ugc_type_life_hand_make\n    UgcType.LifePainting -> R.string.ugc_type_life_painting\n    UgcType.LifeDaily -> R.string.ugc_type_life_daily\n\n    UgcType.Car -> R.string.ugc_type_car\n    UgcType.CarKnowledge -> R.string.ugc_type_car_knowledge\n    UgcType.CarStrategy -> R.string.ugc_type_car_strategy\n    UgcType.CarNewEnergyVehicle -> R.string.ugc_type_car_new_energy_vehicle\n    UgcType.CarRacing -> R.string.ugc_type_car_racing\n    UgcType.CarModifiedVehicle -> R.string.ugc_type_car_modified_vehicle\n    UgcType.CarMotorcycle -> R.string.ugc_type_car_motorcycle\n    UgcType.CarTouringCar -> R.string.ugc_type_car_touring_car\n    UgcType.CarLife -> R.string.ugc_type_car_life\n\n    UgcType.Fashion -> R.string.ugc_type_fashion\n    UgcType.FashionMakeup -> R.string.ugc_type_fashion_makeup\n    UgcType.FashionCos -> R.string.ugc_type_fashion_cos\n    UgcType.FashionClothing -> R.string.ugc_type_fashion_clothing\n    UgcType.FashionCatwalk -> R.string.ugc_type_fashion_catwalk\n\n    UgcType.Sports -> R.string.ugc_type_sports\n    UgcType.SportsBasketball -> R.string.ugc_type_sports_basketball\n    UgcType.SportsFootball -> R.string.ugc_type_sports_football\n    UgcType.SportsAerobics -> R.string.ugc_type_sports_aerobics\n    UgcType.SportsAthletic -> R.string.ugc_type_sports_athletic\n    UgcType.SportsCulture -> R.string.ugc_type_sports_culture\n    UgcType.SportsComprehensive -> R.string.ugc_type_sports_comprehensive\n\n    UgcType.Animal -> R.string.ugc_type_animal\n    UgcType.AnimalCat -> R.string.ugc_type_animal_cat\n    UgcType.AnimalDog -> R.string.ugc_type_animal_dog\n    UgcType.AnimalReptiles -> R.string.ugc_type_animal_reptiles\n    UgcType.AnimalWildAnima -> R.string.ugc_type_animal_wild_anima\n    UgcType.AnimalSecondEdition -> R.string.ugc_type_animal_second_edition\n    UgcType.AnimalComposite -> R.string.ugc_type_animal_composite\n}.stringRes(context)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/UgcTypeV2Extends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.bv.R\n\nfun UgcTypeV2.getDisplayName(context: Context) = when (this) {\n    UgcTypeV2.Douga -> R.string.ugc_type_v2_douga\n    UgcTypeV2.DougaFanAnime -> R.string.ugc_type_v2_douga_fan_anime\n    UgcTypeV2.DougaGarageKit -> R.string.ugc_type_v2_douga_garage_kit\n    UgcTypeV2.DougaCosplay -> R.string.ugc_type_v2_douga_osplay\n    UgcTypeV2.DougaOffline -> R.string.ugc_type_v2_douga_offline\n    UgcTypeV2.DougaEditing -> R.string.ugc_type_v2_douga_editing\n    UgcTypeV2.DougaCommentary -> R.string.ugc_type_v2_douga_commentary\n    UgcTypeV2.DougaQuickView -> R.string.ugc_type_v2_douga_quick_view\n    UgcTypeV2.DougaVoice -> R.string.ugc_type_v2_douga_voice\n    UgcTypeV2.DougaInformation -> R.string.ugc_type_v2_douga_information\n    UgcTypeV2.DougaInterpret -> R.string.ugc_type_v2_douga_interpret\n    UgcTypeV2.DougaVup -> R.string.ugc_type_v2_douga_vup\n    UgcTypeV2.DougaTokusatsu -> R.string.ugc_type_v2_douga_tokusatsu\n    UgcTypeV2.DougaPuppetry -> R.string.ugc_type_v2_douga_puppetry\n    UgcTypeV2.DougaComic -> R.string.ugc_type_v2_douga_comic\n    UgcTypeV2.DougaMotion -> R.string.ugc_type_v2_douga_motion\n    UgcTypeV2.DougaReaction -> R.string.ugc_type_v2_douga_reaction\n    UgcTypeV2.DougaTutorial -> R.string.ugc_type_v2_douga_tutorial\n    UgcTypeV2.DougaOther -> R.string.ugc_type_v2_douga_other\n\n    UgcTypeV2.Game -> R.string.ugc_type_v2_game\n    UgcTypeV2.GameRpg -> R.string.ugc_type_v2_game_rpg\n    UgcTypeV2.GameMmorpg -> R.string.ugc_type_v2_game_mmorpg\n    UgcTypeV2.GameStandAlone -> R.string.ugc_type_v2_game_stand_alone\n    UgcTypeV2.GameSlg -> R.string.ugc_type_v2_game_slg\n    UgcTypeV2.GameTbs -> R.string.ugc_type_v2_game_tbs\n    UgcTypeV2.GameRts -> R.string.ugc_type_v2_game_rts\n    UgcTypeV2.GameMoba -> R.string.ugc_type_v2_game_moba\n    UgcTypeV2.GameStg -> R.string.ugc_type_v2_game_stg\n    UgcTypeV2.GameSpg -> R.string.ugc_type_v2_game_spg\n    UgcTypeV2.GameAct -> R.string.ugc_type_v2_game_act\n    UgcTypeV2.GameMsc -> R.string.ugc_type_v2_game_msc\n    UgcTypeV2.GameSim -> R.string.ugc_type_v2_game_sim\n    UgcTypeV2.GameOtome -> R.string.ugc_type_v2_game_otome\n    UgcTypeV2.GamePuz -> R.string.ugc_type_v2_game_puz\n    UgcTypeV2.GameSandbox -> R.string.ugc_type_v2_game_sandbox\n    UgcTypeV2.GameOther -> R.string.ugc_type_v2_game_other\n\n    UgcTypeV2.Kichiku -> R.string.ugc_type_v2_kichiku\n    UgcTypeV2.KichikuGuide -> R.string.ugc_type_v2_kichiku_guide\n    UgcTypeV2.KichikuTheatre -> R.string.ugc_type_v2_kichiku_theatre\n    UgcTypeV2.KichikuManualVocaloid -> R.string.ugc_type_v2_kichiku_manual_vocaloid\n    UgcTypeV2.KichikuMad -> R.string.ugc_type_v2_kichiku_mad\n    UgcTypeV2.KichikuOther -> R.string.ugc_type_v2_kichiku_other\n\n    UgcTypeV2.Music -> R.string.ugc_type_v2_music\n    UgcTypeV2.MusicOriginal -> R.string.ugc_type_v2_music_original\n    UgcTypeV2.MusicMv -> R.string.ugc_type_v2_music_mv\n    UgcTypeV2.MusicLive -> R.string.ugc_type_v2_music_live\n    UgcTypeV2.MusicFanVideos -> R.string.ugc_type_v2_music_fan_videos\n    UgcTypeV2.MusicCover -> R.string.ugc_type_v2_music_cover\n    UgcTypeV2.MusicPerform -> R.string.ugc_type_v2_music_perform\n    UgcTypeV2.MusicVocaloid -> R.string.ugc_type_v2_music_vocaloid\n    UgcTypeV2.MusicAiMusic -> R.string.ugc_type_v2_music_ai_music\n    UgcTypeV2.MusicRadio -> R.string.ugc_type_v2_music_radio\n    UgcTypeV2.MusicTutorial -> R.string.ugc_type_v2_music_tutorial\n    UgcTypeV2.MusicCommentary -> R.string.ugc_type_v2_music_commentary\n    UgcTypeV2.MusicOther -> R.string.ugc_type_v2_music_other\n\n    UgcTypeV2.Dance -> R.string.ugc_type_v2_dance\n    UgcTypeV2.DanceOtaku -> R.string.ugc_type_v2_dance_otaku\n    UgcTypeV2.DanceHiphop -> R.string.ugc_type_v2_dance_hiphop\n    UgcTypeV2.DanceGestures -> R.string.ugc_type_v2_dance_gestures\n    UgcTypeV2.DanceStar -> R.string.ugc_type_v2_dance_star\n    UgcTypeV2.DanceChina -> R.string.ugc_type_v2_dance_china\n    UgcTypeV2.DanceTutorial -> R.string.ugc_type_v2_dance_tutorial\n    UgcTypeV2.DanceBallet -> R.string.ugc_type_v2_dance_ballet\n    UgcTypeV2.DanceWota -> R.string.ugc_type_v2_dance_wota\n    UgcTypeV2.DanceOther -> R.string.ugc_type_v2_dance_other\n\n    UgcTypeV2.Cinephile -> R.string.ugc_type_v2_cinephile\n    UgcTypeV2.CinephileCommentary -> R.string.ugc_type_v2_cinephile_commentary\n    UgcTypeV2.CinephileMontage -> R.string.ugc_type_v2_cinephile_montage\n    UgcTypeV2.CinephileInformation -> R.string.ugc_type_v2_cinephile_information\n    UgcTypeV2.CinephilePorterage -> R.string.ugc_type_v2_cinephile_porterage\n    UgcTypeV2.CinephileShortFilm -> R.string.ugc_type_v2_cinephile_shortfilm\n    UgcTypeV2.CinephileAi -> R.string.ugc_type_v2_cinephile_ai\n    UgcTypeV2.CinephileReaction -> R.string.ugc_type_v2_cinephile_reaction\n    UgcTypeV2.CinephileOther -> R.string.ugc_type_v2_cinephile_other\n\n    UgcTypeV2.Ent -> R.string.ugc_type_v2_ent\n    UgcTypeV2.EntCommentary -> R.string.ugc_type_v2_ent_commentary\n    UgcTypeV2.EntMontage -> R.string.ugc_type_v2_ent_montage\n    UgcTypeV2.EntFansVideo -> R.string.ugc_type_v2_ent_fans_video\n    UgcTypeV2.EntInformation -> R.string.ugc_type_v2_ent_information\n    UgcTypeV2.EntReaction -> R.string.ugc_type_v2_ent_reaction\n    UgcTypeV2.EntVariety -> R.string.ugc_type_v2_ent_variety\n    UgcTypeV2.EntOther -> R.string.ugc_type_v2_ent_other\n\n    UgcTypeV2.Knowledge -> R.string.ugc_type_v2_knowledge\n    UgcTypeV2.KnowledgeExam -> R.string.ugc_type_v2_knowledge_exam\n    UgcTypeV2.KnowledgeLangSkill -> R.string.ugc_type_v2_knowledge_lang_skill\n    UgcTypeV2.KnowledgeCampus -> R.string.ugc_type_v2_knowledge_campus\n    UgcTypeV2.KnowledgeBusiness -> R.string.ugc_type_v2_knowledge_business\n    UgcTypeV2.KnowledgeSocialObservation -> R.string.ugc_type_v2_knowledge_social_observation\n    UgcTypeV2.KnowledgePolitics -> R.string.ugc_type_v2_knowledge_politics\n    UgcTypeV2.KnowledgeHumanityHistory -> R.string.ugc_type_v2_knowledge_humanity_history\n    UgcTypeV2.KnowledgeDesign -> R.string.ugc_type_v2_knowledge_design\n    UgcTypeV2.KnowledgePsychology -> R.string.ugc_type_v2_knowledge_psychology\n    UgcTypeV2.KnowledgeCareer -> R.string.ugc_type_v2_knowledge_career\n    UgcTypeV2.KnowledgeScience -> R.string.ugc_type_v2_knowledge_science\n    UgcTypeV2.KnowledgeOther -> R.string.ugc_type_v2_knowledge_other\n\n    UgcTypeV2.Tech -> R.string.ugc_type_v2_tech\n    UgcTypeV2.TechComputer -> R.string.ugc_type_v2_tech_computer\n    UgcTypeV2.TechPhone -> R.string.ugc_type_v2_tech_phone\n    UgcTypeV2.TechPad -> R.string.ugc_type_v2_tech_pad\n    UgcTypeV2.TechPhotography -> R.string.ugc_type_v2_tech_photography\n    UgcTypeV2.TechMachine -> R.string.ugc_type_v2_tech_machine\n    UgcTypeV2.TechCreate -> R.string.ugc_type_v2_tech_create\n    UgcTypeV2.TechOther -> R.string.ugc_type_v2_tech_other\n\n    UgcTypeV2.Information -> R.string.ugc_type_v2_information\n    UgcTypeV2.InformationPolitics -> R.string.ugc_type_v2_information_politics\n    UgcTypeV2.InformationOverseas -> R.string.ugc_type_v2_information_overseas\n    UgcTypeV2.InformationSocial -> R.string.ugc_type_v2_information_social\n    UgcTypeV2.InformationOther -> R.string.ugc_type_v2_information_other\n\n    UgcTypeV2.Food -> R.string.ugc_type_v2_food\n    UgcTypeV2.FoodMake -> R.string.ugc_type_v2_food_make\n    UgcTypeV2.FoodDetective -> R.string.ugc_type_v2_food_detective\n    UgcTypeV2.FoodCommentary -> R.string.ugc_type_v2_food_commentary\n    UgcTypeV2.FoodRecord -> R.string.ugc_type_v2_food_record\n    UgcTypeV2.FoodOther -> R.string.ugc_type_v2_food_other\n\n    UgcTypeV2.Shortplay -> R.string.ugc_type_v2_shortplay\n    UgcTypeV2.ShortplayPlot -> R.string.ugc_type_v2_shortplay_plot\n    UgcTypeV2.ShortplayLang -> R.string.ugc_type_v2_shortplay_lang\n    UgcTypeV2.ShortplayUpVariety -> R.string.ugc_type_v2_shortplay_up_variety\n    UgcTypeV2.ShortplayInterview -> R.string.ugc_type_v2_shortplay_interview\n\n    UgcTypeV2.Car -> R.string.ugc_type_v2_car\n    UgcTypeV2.CarCommentary -> R.string.ugc_type_v2_car_commentary\n    UgcTypeV2.CarCulture -> R.string.ugc_type_v2_car_culture\n    UgcTypeV2.CarLife -> R.string.ugc_type_v2_car_life\n    UgcTypeV2.CarTech -> R.string.ugc_type_v2_car_tech\n    UgcTypeV2.CarOther -> R.string.ugc_type_v2_car_other\n\n    UgcTypeV2.Fashion -> R.string.ugc_type_v2_fashion\n    UgcTypeV2.FashionMakeup -> R.string.ugc_type_v2_fashion_makeup\n    UgcTypeV2.FashionSkincare -> R.string.ugc_type_v2_fashion_skincare\n    UgcTypeV2.FashionCos -> R.string.ugc_type_v2_fashion_cos\n    UgcTypeV2.FashionOutfits -> R.string.ugc_type_v2_fashion_outfits\n    UgcTypeV2.FashionAccessories -> R.string.ugc_type_v2_fashion_accessories\n    UgcTypeV2.FashionJewelry -> R.string.ugc_type_v2_fashion_jewelry\n    UgcTypeV2.FashionTrick -> R.string.ugc_type_v2_fashion_trick\n    UgcTypeV2.FashionCommentary -> R.string.ugc_type_v2_fashion_commentary\n    UgcTypeV2.FashionOther -> R.string.ugc_type_v2_fashion_other\n\n    UgcTypeV2.Sports -> R.string.ugc_type_v2_sports\n    UgcTypeV2.SportsTrend -> R.string.ugc_type_v2_sports_trend\n    UgcTypeV2.SportsFootball -> R.string.ugc_type_v2_sports_football\n    UgcTypeV2.SportsBasketball -> R.string.ugc_type_v2_sports_basketball\n    UgcTypeV2.SportsRunning -> R.string.ugc_type_v2_sports_running\n    UgcTypeV2.SportsKungfu -> R.string.ugc_type_v2_sports_kungfu\n    UgcTypeV2.SportsFighting -> R.string.ugc_type_v2_sports_fighting\n    UgcTypeV2.SportsBadminton -> R.string.ugc_type_v2_sports_badminton\n    UgcTypeV2.SportsInformation -> R.string.ugc_type_v2_sports_information\n    UgcTypeV2.SportsMatch -> R.string.ugc_type_v2_sports_match\n    UgcTypeV2.SportsOther -> R.string.ugc_type_v2_sports_other\n\n    UgcTypeV2.Animal -> R.string.ugc_type_v2_animal\n    UgcTypeV2.AnimalCat -> R.string.ugc_type_v2_animal_cat\n    UgcTypeV2.AnimalDog -> R.string.ugc_type_v2_animal_dog\n    UgcTypeV2.AnimalReptiles -> R.string.ugc_type_v2_animal_reptiles\n    UgcTypeV2.AnimalScience -> R.string.ugc_type_v2_animal_science\n    UgcTypeV2.AnimalOther -> R.string.ugc_type_v2_animal_other\n\n    UgcTypeV2.Vlog -> R.string.ugc_type_v2_vlog\n    UgcTypeV2.VlogLife -> R.string.ugc_type_v2_vlog_life\n    UgcTypeV2.VlogStudent -> R.string.ugc_type_v2_vlog_student\n    UgcTypeV2.VlogCareer -> R.string.ugc_type_v2_vlog_career\n    UgcTypeV2.VlogOther -> R.string.ugc_type_v2_vlog_other\n\n    UgcTypeV2.Painting -> R.string.ugc_type_v2_painting\n    UgcTypeV2.PaintingAcg -> R.string.ugc_type_v2_painting_acg\n    UgcTypeV2.PaintingNoneAcg -> R.string.ugc_type_v2_painting_none_acg\n    UgcTypeV2.PaintingTutorial -> R.string.ugc_type_v2_painting_tutorial\n    UgcTypeV2.PaintingOther -> R.string.ugc_type_v2_painting_other\n\n    UgcTypeV2.Ai -> R.string.ugc_type_v2_ai\n    UgcTypeV2.AiTutorial -> R.string.ugc_type_v2_ai_tutorial\n    UgcTypeV2.AiInformation -> R.string.ugc_type_v2_ai_information\n    UgcTypeV2.AiOther -> R.string.ugc_type_v2_ai_other\n\n    UgcTypeV2.Home -> R.string.ugc_type_v2_home\n    UgcTypeV2.HomeTrade -> R.string.ugc_type_v2_home_trade\n    UgcTypeV2.HomeRenovation -> R.string.ugc_type_v2_home_renovation\n    UgcTypeV2.HomeFurniture -> R.string.ugc_type_v2_home_furniture\n    UgcTypeV2.HomeAppliances -> R.string.ugc_type_v2_home_appliances\n\n    UgcTypeV2.Outdoors -> R.string.ugc_type_v2_outdoors\n    UgcTypeV2.OutdoorsCamping -> R.string.ugc_type_v2_outdoors_camping\n    UgcTypeV2.OutdoorsHiking -> R.string.ugc_type_v2_outdoors_hiking\n    UgcTypeV2.OutdoorsExplore -> R.string.ugc_type_v2_outdoors_explore\n    UgcTypeV2.OutdoorsOther -> R.string.ugc_type_v2_outdoors_other\n\n    UgcTypeV2.Gym -> R.string.ugc_type_v2_gym\n    UgcTypeV2.GymScience -> R.string.ugc_type_v2_gym_science\n    UgcTypeV2.GymTutorial -> R.string.ugc_type_v2_gym_tutorial\n    UgcTypeV2.GymRecord -> R.string.ugc_type_v2_gym_record\n    UgcTypeV2.GymFigure -> R.string.ugc_type_v2_gym_figure\n    UgcTypeV2.GymOther -> R.string.ugc_type_v2_gym_other\n\n    UgcTypeV2.Handmake -> R.string.ugc_type_v2_handmake\n    UgcTypeV2.HandmakeHandbook -> R.string.ugc_type_v2_handmake_handbook\n    UgcTypeV2.HandmakeLight -> R.string.ugc_type_v2_handmake_light\n    UgcTypeV2.HandmakeTraditional -> R.string.ugc_type_v2_handmake_traditional\n    UgcTypeV2.HandmakeRelief -> R.string.ugc_type_v2_handmake_relief\n    UgcTypeV2.HandmakeDiy -> R.string.ugc_type_v2_handmake_diy\n    UgcTypeV2.HandmakeOther -> R.string.ugc_type_v2_handmake_other\n\n    UgcTypeV2.Travel -> R.string.ugc_type_v2_travel\n    UgcTypeV2.TravelRecord -> R.string.ugc_type_v2_travel_record\n    UgcTypeV2.TravelStrategy -> R.string.ugc_type_v2_travel_strategy\n    UgcTypeV2.TravelCity -> R.string.ugc_type_v2_travel_city\n    UgcTypeV2.TravelTransport -> R.string.ugc_type_v2_travel_transport\n\n    UgcTypeV2.Rural -> R.string.ugc_type_v2_rural\n    UgcTypeV2.RuralPlanting -> R.string.ugc_type_v2_rural_planting\n    UgcTypeV2.RuralFishing -> R.string.ugc_type_v2_rural_fishing\n    UgcTypeV2.RuralHarvest -> R.string.ugc_type_v2_rural_harvest\n    UgcTypeV2.RuralTech -> R.string.ugc_type_v2_rural_tech\n    UgcTypeV2.RuralLife -> R.string.ugc_type_v2_rural_life\n\n    UgcTypeV2.Parenting -> R.string.ugc_type_v2_parenting\n    UgcTypeV2.ParentingPregnantCare -> R.string.ugc_type_v2_parenting_pregnant_care\n    UgcTypeV2.ParentingInfantCare -> R.string.ugc_type_v2_parenting_infant_care\n    UgcTypeV2.ParentingTalent -> R.string.ugc_type_v2_parenting_talent\n    UgcTypeV2.ParentingCute -> R.string.ugc_type_v2_parenting_cute\n    UgcTypeV2.ParentingInteraction -> R.string.ugc_type_v2_parenting_interaction\n    UgcTypeV2.ParentingEducation -> R.string.ugc_type_v2_parenting_education\n    UgcTypeV2.ParentingOther -> R.string.ugc_type_v2_parenting_other\n\n    UgcTypeV2.Health -> R.string.ugc_type_v2_health\n    UgcTypeV2.HealthScience -> R.string.ugc_type_v2_health_science\n    UgcTypeV2.HealthRegimen -> R.string.ugc_type_v2_health_regimen\n    UgcTypeV2.HealthSexes -> R.string.ugc_type_v2_health_sexes\n    UgcTypeV2.HealthPsychology -> R.string.ugc_type_v2_health_psychology\n    UgcTypeV2.HealthAsmr -> R.string.ugc_type_v2_health_asmr\n    UgcTypeV2.HealthOther -> R.string.ugc_type_v2_health_other\n\n    UgcTypeV2.Emotion -> R.string.ugc_type_v2_emotion\n    UgcTypeV2.EmotionFamily -> R.string.ugc_type_v2_emotion_family\n    UgcTypeV2.EmotionRomantic -> R.string.ugc_type_v2_emotion_romantic\n    UgcTypeV2.EmotionInterpersonal -> R.string.ugc_type_v2_emotion_interpersonal\n    UgcTypeV2.EmotionGrowth -> R.string.ugc_type_v2_emotion_growth\n\n    UgcTypeV2.LifeJoy -> R.string.ugc_type_v2_life_joy\n    UgcTypeV2.LifeJoyLeisure -> R.string.ugc_type_v2_life_joy_leisure\n    UgcTypeV2.LifeJoyOnSite -> R.string.ugc_type_v2_life_joy_on_site\n    UgcTypeV2.LifeJoyArtisticProducts -> R.string.ugc_type_v2_life_joy_artistic_products\n    UgcTypeV2.LifeJoyTrendyToys -> R.string.ugc_type_v2_life_joy_trendy_toys\n    UgcTypeV2.LifeJoyOther -> R.string.ugc_type_v2_life_joy_other\n\n    UgcTypeV2.LifeExperience -> R.string.ugc_type_v2_life_experience\n    UgcTypeV2.LifeExperienceSkills -> R.string.ugc_type_v2_life_experience_skills\n    UgcTypeV2.LifeExperienceProcedures -> R.string.ugc_type_v2_life_experience_procedures\n    UgcTypeV2.LifeExperienceMarriage -> R.string.ugc_type_v2_life_experience_marriage\n\n    UgcTypeV2.Mysticism -> R.string.ugc_type_v2_mysticism\n    UgcTypeV2.MysticismTarot -> R.string.ugc_type_v2_mysticism_tarot\n    UgcTypeV2.MysticismHoroscope -> R.string.ugc_type_v2_mysticism_horoscope\n    UgcTypeV2.MysticismMetaphysics -> R.string.ugc_type_v2_mysticism_metaphysics\n    UgcTypeV2.MysticismHealing -> R.string.ugc_type_v2_mysticism_healing\n    UgcTypeV2.MysticismOther -> R.string.ugc_type_v2_mysticism_other\n}.stringRes(context)\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/util/calculateWindowSizeClassInPreview.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi\nimport androidx.compose.material3.windowsizeclass.WindowSizeClass\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\n\n@ExperimentalMaterial3WindowSizeClassApi\n@Composable\nfun calculateWindowSizeClassInPreview(): WindowSizeClass {\n    val configuration = LocalConfiguration.current\n    val size = DpSize(configuration.screenWidthDp.dp, configuration.screenHeightDp.dp)\n    return WindowSizeClass.calculateFromSize(size)\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/CommentViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.biliapi.entity.reply.CommentPage\nimport dev.aaa1115910.biliapi.entity.reply.CommentReplyPage\nimport dev.aaa1115910.biliapi.entity.reply.CommentSort\nimport dev.aaa1115910.biliapi.repositories.CommentRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fException\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass CommentViewModel(\n    private val commentRepository: CommentRepository\n) : ViewModel() {\n    companion object {\n        val logger = KotlinLogging.logger {}\n    }\n\n    var commentId = 0L\n    var commentType = 0L\n\n    val comments = mutableStateListOf<Comment>()\n    val replies = mutableStateListOf<Comment>()\n    var replyRootComment by mutableStateOf<Comment?>(null)\n\n    var rpid by mutableLongStateOf(0L)\n    var rpCount by mutableIntStateOf(0)\n\n    var nextCommentPage = CommentPage()\n    var nextCommentReplyPage = CommentReplyPage()\n\n    var commentSort by mutableStateOf(CommentSort.Hot)\n    var replySort by mutableStateOf(CommentSort.Time)\n\n    var hasMoreComments by mutableStateOf(true)\n    var hasMoreReplies by mutableStateOf(true)\n    var refreshingComments by mutableStateOf(false)\n    var refreshingReplies by mutableStateOf(false)\n    var updatingComments by mutableStateOf(false)\n    var updatingReplies by mutableStateOf(false)\n\n    suspend fun loadMoreComment() {\n        if (updatingComments) return\n        withContext(Dispatchers.Main) {\n            updatingComments = true\n        }\n        if (!hasMoreComments) {\n            withContext(Dispatchers.Main) {\n                updatingComments = false\n            }\n            delay(300)\n            withContext(Dispatchers.Main) {\n                refreshingComments = false\n            }\n            return\n        }\n        logger.fInfo { \"Load more comments: [commentId=$commentId, commentType=$commentType, page=$nextCommentPage]\" }\n        runCatching {\n            val commentsData = commentRepository.getComments(\n                id = commentId,\n                type = commentType,\n                page = nextCommentPage,\n                sort = commentSort,\n                preferApiType = Prefs.apiType\n            )\n            nextCommentPage = commentsData.nextPage\n            hasMoreComments = commentsData.hasNext\n            comments.addAll(commentsData.comments)\n        }.onFailure {\n            logger.fException(it) { \"Load more comments failed\" }\n            withContext(Dispatchers.Main) {\n                \"加载评论失败：${it.localizedMessage}\".toast(BVApp.context)\n            }\n        }\n        withContext(Dispatchers.Main) {\n            updatingComments = false\n        }\n        delay(300)\n        withContext(Dispatchers.Main) {\n            refreshingComments = false\n        }\n    }\n\n    suspend fun switchCommentSort(newSort: CommentSort) {\n        logger.fInfo { \"Switch comment sort to ${newSort.name}\" }\n        commentSort = newSort\n        refreshComments()\n    }\n\n    suspend fun refreshComments() {\n        refreshingComments = true\n        logger.fInfo { \"refresh comments\" }\n        nextCommentPage = CommentPage()\n        hasMoreComments = true\n        comments.clear()\n        loadMoreComment()\n    }\n\n    suspend fun loadMoreReplies() {\n        if (updatingReplies) return\n        withContext(Dispatchers.Main) {\n            updatingReplies = true\n        }\n        if (!hasMoreReplies) {\n            withContext(Dispatchers.Main) {\n                updatingReplies = false\n            }\n            delay(300)\n            withContext(Dispatchers.Main) {\n                refreshingReplies = false\n            }\n            return\n        }\n        logger.fInfo { \"Load more replies, commentId=$commentId, commentType=$commentType, page=$nextCommentReplyPage\" }\n        runCatching {\n            val commentRepliesData = commentRepository.getCommentReplies(\n                rpid = rpid,\n                type = commentType,\n                commentId = commentId,\n                page = nextCommentReplyPage,\n                sort = replySort,\n                preferApiType = Prefs.apiType\n            )\n            nextCommentReplyPage = commentRepliesData.nextPage\n            hasMoreReplies = commentRepliesData.hasNext\n            if (replyRootComment == null) replyRootComment = commentRepliesData.rootComment\n            replies.addAll(commentRepliesData.replies)\n        }.onFailure {\n            logger.fException(it) { \"Load more replies failed\" }\n        }\n        withContext(Dispatchers.Main) {\n            updatingReplies = false\n        }\n        delay(300)\n        withContext(Dispatchers.Main) {\n            refreshingReplies = false\n        }\n    }\n\n    suspend fun switchReplySort(newSort: CommentSort) {\n        logger.fInfo { \"Switch reply sort to ${newSort.name}\" }\n        replySort = newSort\n        refreshReplies()\n    }\n\n    suspend fun refreshReplies() {\n        refreshingReplies = true\n        logger.fInfo { \"refresh replies\" }\n        nextCommentReplyPage = CommentReplyPage()\n        hasMoreReplies = true\n        replies.clear()\n        loadMoreReplies()\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/DynamicDetailViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport dev.aaa1115910.biliapi.entity.user.DynamicItem\nimport dev.aaa1115910.biliapi.repositories.UserRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fException\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass DynamicDetailViewModel(\n    private val userRepository: UserRepository\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    var dynamicId by mutableStateOf(\"\")\n    var dynamicItem by mutableStateOf<DynamicItem?>(null)\n\n    suspend fun loadDynamic() {\n        logger.fInfo { \"Loading dynamic detail: $dynamicId\" }\n        runCatching {\n            dynamicItem = userRepository.getDynamicDetail(\n                dynamicId = dynamicId,\n                preferApiType = Prefs.apiType\n            )\n        }.onFailure {\n            logger.fException(it) { \"Failed to load dynamic\" }\n            withContext(Dispatchers.Main) {\n                \"Failed to load dynamic: ${it.message}\".toast(BVApp.context)\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/SeasonViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.video.season.PgcSeason\nimport dev.aaa1115910.biliapi.entity.video.season.SeasonDetail\nimport dev.aaa1115910.biliapi.repositories.UserRepository\nimport dev.aaa1115910.biliapi.repositories.VideoDetailRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.swapList\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\nimport java.util.UUID\n\n@KoinViewModel\nclass SeasonViewModel(\n    private val videoDetailRepository: VideoDetailRepository,\n    private val userRepository: UserRepository\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger {}\n    }\n\n    var seasonId by mutableStateOf<Int?>(null)\n    var epId by mutableStateOf<Int?>(null)\n    var proxyArea by mutableStateOf(ProxyArea.MainLand)\n\n    val seasons = mutableStateListOf<PgcSeason>()\n    var seasonData by mutableStateOf<SeasonDetail?>(null)\n\n    var isFollowing by mutableStateOf(false)\n    var lastPlayProgress by mutableStateOf<SeasonDetail.UserStatus.Progress?>(null)\n\n    var tip by mutableStateOf(\"Loading\")\n\n    val uuid: String = UUID.randomUUID().toString()\n\n    suspend fun updateSeasonData() {\n        runCatching {\n            val data = videoDetailRepository.getPgcVideoDetail(\n                seasonId = seasonId,\n                epid = epId,\n                preferApiType = if (proxyArea != ProxyArea.MainLand) ApiType.App else Prefs.apiType\n            )\n            seasonData = data.copy()\n            seasons.swapList(data.seasons)\n            isFollowing = data.userStatus.follow\n            lastPlayProgress = data.userStatus.progress\n            logger.fInfo { \"Get season info success, seasonData: ${seasonData}\" }\n        }.onFailure {\n            tip = it.localizedMessage ?: \"未知错误\"\n            logger.fInfo { \"Get season info failed: ${it.stackTraceToString()}\" }\n        }\n    }\n\n    suspend fun updateLastPlayProgress() {\n        //延迟 200ms，避免获取到的依旧是旧数据\n        delay(200)\n        runCatching {\n            val data = videoDetailRepository.getPgcVideoDetail(\n                seasonId = seasonId,\n                epid = epId,\n                preferApiType = if (proxyArea != ProxyArea.MainLand) ApiType.App else Prefs.apiType\n            ).userStatus.progress\n            withContext(Dispatchers.Main) { lastPlayProgress = data }\n            logger.info { \"update user status progress: $lastPlayProgress\" }\n        }.onFailure {\n            logger.fInfo { \"update user status progress failed: ${it.stackTraceToString()}\" }\n        }\n    }\n\n    suspend fun followSeason() {\n        runCatching {\n            val resultToast = userRepository.addSeasonFollow(\n                seasonId = seasonData?.seasonId ?: return@runCatching,\n                preferApiType = Prefs.apiType\n            )\n            withContext(Dispatchers.Main) {\n                isFollowing = true\n                resultToast.toast(BVApp.context)\n            }\n        }.onFailure {\n            logger.fInfo { \"Add season follow failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                R.string.follow_bangumi_enable_fail.toast(BVApp.context)\n            }\n        }\n    }\n\n    suspend fun unFollowSeason() {\n        runCatching {\n            val resultToast = userRepository.delSeasonFollow(\n                seasonId = seasonData?.seasonId ?: return@runCatching,\n                preferApiType = Prefs.apiType\n            )\n            withContext(Dispatchers.Main) {\n                isFollowing = false\n                resultToast.toast(BVApp.context)\n            }\n        }.onFailure {\n            logger.fInfo { \"Del season follow failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                R.string.follow_bangumi_disable_fail.toast(BVApp.context)\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/TagViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.ugc.toSmartDate\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.util.addWithMainContext\nimport dev.aaa1115910.bv.util.fInfo\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass TagViewModel : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    var tagName by mutableStateOf(\"\")\n    var tagId by mutableIntStateOf(0)\n    var topVideos = mutableStateListOf<VideoCardData>()\n\n    private var pageNumber = 1\n    private var pageSize = 40\n    private var total = -1\n\n    private var updating = false\n    var noMore = false\n\n    fun update() {\n        viewModelScope.launch(Dispatchers.Default) {\n            updateTagVideos()\n        }\n    }\n\n    private suspend fun updateTagVideos() {\n        if (total != -1 && pageNumber * pageSize >= total) {\n            noMore = true\n            return\n        }\n        if (updating) return\n        updating = true\n        runCatching {\n            val response = BiliHttpApi.getTagTopVideos(\n                tagId = tagId,\n                pageNumber = pageNumber,\n                pageSize = pageSize\n            )\n            total = response.total\n            val videoList = response.data\n            if (videoList.isEmpty()) noMore = true\n            videoList.forEach { tagVideoItem ->\n                topVideos.addWithMainContext(\n                    VideoCardData(\n                        avid = tagVideoItem.aid,\n                        title = tagVideoItem.title,\n                        cover = tagVideoItem.pic,\n                        upName = tagVideoItem.owner.name,\n                        upId = tagVideoItem.owner.mid,\n                        play = tagVideoItem.stat.view,\n                        danmaku = tagVideoItem.stat.danmaku,\n                        time = tagVideoItem.duration * 1000L,\n                        pubTime = tagVideoItem.pubdate.toLong().toSmartDate()\n                    )\n                )\n            }\n            logger.fInfo { \"Update tag top videos success\" }\n        }.onFailure {\n            logger.fInfo { \"Update tag top videos failed: ${it.stackTraceToString()}\" }\n        }.onSuccess {\n            pageNumber++\n        }\n        updating = false\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/UserSwitchViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.dao.AppDatabase\nimport dev.aaa1115910.bv.entity.db.UserDB\nimport dev.aaa1115910.bv.repository.UserRepository\nimport dev.aaa1115910.bv.util.Prefs\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UserSwitchViewModel(\n    private val userRepository: UserRepository,\n    private val db: AppDatabase = BVApp.getAppDatabase()\n) : ViewModel() {\n    var loading by mutableStateOf(true)\n    var currentUser by mutableStateOf(UserDB(-1, -1, \"\", \"\", \"\"))\n    val userDbList = mutableStateListOf<UserDB>()\n\n    fun updateData() {\n        viewModelScope.launch(Dispatchers.IO) {\n            updateUserDbList()\n            withContext(Dispatchers.Main) { loading = false }\n        }\n    }\n\n    suspend fun updateUserDbList() {\n        withContext(Dispatchers.Main) {\n            userDbList.clear()\n            userDbList.addAll(db.userDao().getAll())\n            currentUser = userDbList.find { it.uid == Prefs.uid } ?: UserDB(-1, -1, \"\", \"\", \"\")\n        }\n    }\n\n    suspend fun switchUser(user: UserDB) {\n        userRepository.setUser(user)\n    }\n\n    suspend fun deleteUser(userDB: UserDB) {\n        db.userDao().delete(userDB)\n        updateUserDbList()\n        if (userDbList.isNotEmpty()) {\n            switchUser(userDbList.first())\n        } else {\n            userRepository.logout()\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/UserViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.biliapi.http.entity.AuthFailureException\nimport dev.aaa1115910.biliapi.http.entity.user.MyInfoData\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.repository.UserRepository\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UserViewModel(\n    private val userRepository: UserRepository\n) : ViewModel() {\n    private var shouldUpdateInfo = true\n    private val logger = KotlinLogging.logger { }\n    val isLogin get() = userRepository.isLogin\n    val username get() = userRepository.username\n    val face get() = userRepository.avatar\n\n    var responseData: MyInfoData? by mutableStateOf(null)\n\n    fun updateUserInfo(forceUpdate: Boolean = false) {\n        if (!forceUpdate) {\n            if (!shouldUpdateInfo || !userRepository.isLogin) return\n        } else {\n            if (!userRepository.isLogin) return\n        }\n        logger.fInfo { \"Update user info\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            userRepository.reloadAvatar()\n\n            runCatching {\n                val data = BiliHttpApi.getUserSelfInfo(sessData = Prefs.sessData).getResponseData()\n                withContext(Dispatchers.Main) { responseData = data }\n                logger.fInfo { \"Update user info success\" }\n                shouldUpdateInfo = false\n                userRepository.username = responseData!!.name\n                userRepository.avatar = responseData!!.face\n            }.onFailure {\n                when (it) {\n                    is AuthFailureException -> {\n                        withContext(Dispatchers.Main) {\n                            BVApp.context.getString(R.string.exception_auth_failure)\n                                .toast(BVApp.context)\n                        }\n                        logger.fInfo { \"User auth failure\" }\n                        if (!BuildConfig.DEBUG) userRepository.logout()\n                    }\n\n                    else -> {\n                        withContext(Dispatchers.Main) {\n                            \"获取用户信息失败：${it.message}\".toast(BVApp.context)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    fun logout() {\n        viewModelScope.launch(Dispatchers.IO) {\n            userRepository.logout()\n        }\n    }\n\n    fun clearUserInfo() {\n        userRepository.username = \"\"\n        userRepository.avatar = \"\"\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/VideoPlayerV3ViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel\n\nimport android.net.Uri\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.bv.player.danmaku.DanmakuView\nimport dev.aaa1115910.bv.player.danmaku.model.Danmaku\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.PlayData\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskSegment\nimport dev.aaa1115910.biliapi.http.entity.video.ClipInfo\nimport dev.aaa1115910.biliapi.entity.video.HeartbeatVideoType\nimport dev.aaa1115910.biliapi.entity.video.Subtitle\nimport dev.aaa1115910.biliapi.entity.video.SubtitleAiStatus\nimport dev.aaa1115910.biliapi.entity.video.SubtitleAiType\nimport dev.aaa1115910.biliapi.entity.video.SubtitleType\nimport dev.aaa1115910.biliapi.entity.video.VideoShot\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.biliapi.http.BiliLiveHttpApi\nimport dev.aaa1115910.biliapi.http.entity.VVoucherException\nimport dev.aaa1115910.biliapi.http.entity.live.DanmakuEvent\nimport dev.aaa1115910.biliapi.http.entity.live.OnlineRankCountEvent\nimport dev.aaa1115910.biliapi.http.entity.live.PopularityChangeEvent\nimport dev.aaa1115910.biliapi.repositories.VideoPlayRepository\nimport dev.aaa1115910.biliapi.websocket.LiveDataWebSocket\nimport dev.aaa1115910.bilisubtitle.SubtitleParser\nimport dev.aaa1115910.bilisubtitle.entity.SubtitleItem\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.player.AbstractVideoPlayer\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.DanmakuType\nimport dev.aaa1115910.bv.player.entity.DefaultSubtitle\nimport dev.aaa1115910.bv.player.entity.LiveCodec\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.PlayerDefaultStartPosition\nimport dev.aaa1115910.bv.player.entity.PortraitVideoFixMode\nimport dev.aaa1115910.bv.player.entity.RequestState\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoAspectRatio\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.player.entity.VideoListItemData\nimport dev.aaa1115910.bv.player.entity.VideoRotation\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fError\nimport dev.aaa1115910.bv.util.fException\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.fWarn\nimport dev.aaa1115910.bv.util.LiveStreamUrlFetcher\nimport dev.aaa1115910.bv.util.fDebug\nimport dev.aaa1115910.bv.util.swapList\nimport dev.aaa1115910.bv.util.swapListWithMainContext\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.ktor.client.HttpClient\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.request.get\nimport io.ktor.client.statement.bodyAsText\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\nimport dev.aaa1115910.biliapi.repositories.AuthRepository\nimport dev.aaa1115910.bv.player.entity.NextVideoStrategy\nimport java.net.URI\n\n@KoinViewModel\nclass VideoPlayerV3ViewModel(\n    private val videoInfoRepository: VideoInfoRepository,\n    private val videoPlayRepository: VideoPlayRepository,\n    private val authRepository: AuthRepository,\n) : ViewModel() {\n    private val logger = KotlinLogging.logger { }\n\n    private var videoPlayerState: AbstractVideoPlayer? by mutableStateOf(null)\n    var videoPlayer: AbstractVideoPlayer?\n        get() = videoPlayerState\n        set(value) {\n            value?.onSeek = ::onVideoSeeked\n            value?.onDecoderError = ::fallbackToLowerQuality\n            videoPlayerState = value\n        }\n    var danmakuView: DanmakuView? by mutableStateOf(null)\n    var show by mutableStateOf(false)\n\n    override fun onCleared() {\n        super.onCleared()\n        logger.fInfo { \"VideoPlayerV3ViewModel onCleared\" }\n        releasePlayerResources(\"onCleared\")\n    }\n\n    /**\n     * 释放播放器和弹幕相关资源。幂等，可多次安全调用。\n     * 由 Activity.onDestroy（立即释放重资源）和 ViewModel.onCleared 共同调用。\n     */\n    fun releasePlayerResources(caller: String = \"unknown\") {\n        logger.fInfo { \"releasePlayerResources called by $caller\" }\n\n        stopDanmakuSegmentLoading()\n\n        // 清理直播重连任务\n        liveRetryJob?.cancel()\n        liveRetryJob = null\n\n        // 清理直播URL刷新任务\n        liveUrlRefreshJob?.cancel()\n        liveUrlRefreshJob = null\n\n        // 清理点播CDN URL刷新任务\n        cancelPlayUrlAutoRefresh(caller)\n\n        // 清理直播弹幕资源\n        stopLiveDanmaku()\n\n        try {\n            videoPlayer?.release()\n            videoPlayer = null\n        } catch (e: Exception) {\n            logger.fError { \"Error releasing video player: ${e.message}\" }\n        }\n\n        try {\n            danmakuView?.release()\n            danmakuView = null\n            danmakuMasks.clear()\n        } catch (e: Exception) {\n            logger.fError { \"Error releasing danmaku player: ${e.message}\" }\n        }\n\n        // 清除可能未被GC回收的资源\n        currentSubtitleData.clear()\n    }\n\n    var loadState by mutableStateOf(RequestState.Ready)\n    var errorMessage by mutableStateOf(\"\")\n\n    private var playData: PlayData? by mutableStateOf(null)\n    val danmakuMasks = mutableStateListOf<DanmakuMaskSegment>()\n    var videoShot: VideoShot? by mutableStateOf(null)\n    var clipInfoList: List<ClipInfo> by mutableStateOf(emptyList())\n\n    var availableQuality = mutableStateListOf<Resolution>()\n    var availableVideoCodec = mutableStateListOf<VideoCodec>()\n    var availableSubtitle = mutableStateListOf<Subtitle>()\n    var availableAudio = mutableStateListOf<Audio>()\n    val availableVideoList get() = videoInfoRepository.videoList\n    val preloadedVideoList get() = videoInfoRepository.preloadedVideoList\n    val relatedVideos get() =  videoInfoRepository.relatedVideos\n    val videoDescription get() = videoInfoRepository.description\n    val videoTags get() = videoInfoRepository.tags\n\n    fun resolveLastPreloadedVideoIndex(avid: Long = currentAid): Int {\n        return videoInfoRepository.resolveLastPreloadedVideoIndex(avid)\n    }\n\n    var currentVideoHeight by mutableIntStateOf(0)\n    var currentVideoWidth by mutableIntStateOf(0)\n\n    var currentQuality by mutableStateOf(Prefs.defaultQuality)\n    var currentVideoCodec by mutableStateOf(Prefs.defaultVideoCodec)\n    var currentPlaySpeed by mutableFloatStateOf(Prefs.currentPlaySpeed)\n    var currentVideoAspectRatio by mutableStateOf(VideoAspectRatio.Default)\n    var currentVideoRotation by mutableStateOf(VideoRotation.Original)\n    var currentAudio by mutableStateOf(Prefs.defaultAudio)\n    var currentDanmakuScale by mutableFloatStateOf(Prefs.defaultDanmakuScale)\n    var currentDanmakuOpacity by mutableFloatStateOf(Prefs.defaultDanmakuOpacity)\n    var currentDanmakuEnabled by mutableStateOf(Prefs.defaultDanmakuEnabled)\n    val currentDanmakuTypes = mutableStateListOf<DanmakuType>().apply {\n        addAll(Prefs.defaultDanmakuTypes)\n    }\n    var currentDanmakuArea by mutableFloatStateOf(Prefs.defaultDanmakuArea)\n    var currentDanmakuMask by mutableStateOf(Prefs.defaultDanmakuMask)\n    var currentDanmakuRollingDurationFactor by mutableFloatStateOf(Prefs.defaultDanmakuRollingDurationFactor)\n    var currentDanmakuFilterLevel by mutableIntStateOf(Prefs.defaultDanmakuFilterLevel)\n    var currentLiveDanmakuFilterLevel by mutableIntStateOf(Prefs.defaultLiveDanmakuFilterLevel)\n    var currentSubtitleId by mutableLongStateOf(-1L)\n    var currentSubtitleData = mutableStateListOf<SubtitleItem>()\n    var currentSubtitleType by mutableStateOf(SubtitleType.CC)\n    var currentSubtitleFontSize by mutableStateOf(Prefs.defaultSubtitleFontSize)\n    var currentSubtitleBackgroundOpacity by mutableFloatStateOf(Prefs.defaultSubtitleBackgroundOpacity)\n    var currentSubtitleBottomPadding by mutableStateOf(Prefs.defaultSubtitleBottomPadding)\n\n    var currentPlayMode by mutableStateOf(Prefs.defaultPlayMode)\n\n    var title by mutableStateOf(\"\")\n    var partTitle by mutableStateOf(\"\")\n    var lastPlayed by mutableIntStateOf(0)\n    var fromSeason by mutableStateOf(false)\n    var subType by mutableIntStateOf(0)\n    var epid by mutableIntStateOf(0)\n    var seasonId by mutableIntStateOf(0)\n    var isVerticalVideo by mutableStateOf(false)\n    var proxyArea by mutableStateOf(ProxyArea.MainLand)\n    var play by mutableLongStateOf(0)\n    var danmaku by mutableStateOf(0)\n    var like by mutableStateOf(0)\n\n    // 直播相关属性\n    var isLive by mutableStateOf(false)\n    var liveRoomId by mutableIntStateOf(0)\n    var liveStreamUrl by mutableStateOf(\"\")\n\n    // 直播画质管理\n    var availableLiveQualities = mutableStateListOf<Pair<Int, String>>() // qn -> description\n    var currentLiveQn by mutableIntStateOf(0)\n    var currentLiveQualityDescription by mutableStateOf(\"\")\n    private var liveQnDescMap: Map<Int, String> = emptyMap()\n\n    // 直播编码管理\n    var currentLiveCodec by mutableStateOf(Prefs.defaultLiveCodec)\n\n    // 直播流URL过期时间（毫秒时间戳）\n    var liveStreamExpiresAt by mutableLongStateOf(0L)\n\n    // 直播自动重连\n    private var liveRetryJob: Job? = null\n\n    // 直播URL主动刷新\n    private var liveUrlRefreshJob: Job? = null\n    private var consecutiveRefreshFailures = 0\n\n    // 点播CDN URL自动刷新（修复CDN有效期2h导致长视频无法播放的问题）\n    private var playUrlAutoRefreshJob: Job? = null\n    private var playUrlAutoRefreshToken: Int = 0\n    private var previewTipJob: Job? = null\n\n    companion object {\n        // 提前刷新的时间（毫秒），默认60秒\n        private const val REFRESH_BEFORE_EXPIRY_MS = 60_000L\n        // 最小刷新间隔（毫秒），防止频繁刷新\n        private const val MIN_REFRESH_INTERVAL_MS = 30_000L\n        // 刷新失败后的重试间隔（毫秒）\n        private const val REFRESH_RETRY_INTERVAL_MS = 10_000L\n        // 最大连续刷新失败次数\n        private const val MAX_REFRESH_FAILURES = 3\n\n        // 点播CDN URL自动刷新常量\n        // 在CDN URL过期前提前刷新的时间\n        private const val PLAYURL_AUTO_REFRESH_LEAD_MS = 60_000L\n        // 当无法从URL解析过期时间时，视频时长超过此值才启用回退刷新\n        private const val PLAYURL_AUTO_REFRESH_FALLBACK_MIN_DURATION_MS = 60 * 60_000L\n        // 回退刷新延迟（无法解析deadline时使用）\n        private const val PLAYURL_AUTO_REFRESH_FALLBACK_DELAY_MS = 100 * 60_000L\n        // 两次刷新之间的最小间隔\n        private const val PLAYURL_AUTO_REFRESH_MIN_RELOAD_INTERVAL_MS = 30_000L\n\n        private const val DANMAKU_SEGMENT_DURATION_MS = 6 * 60 * 1000L\n        private const val DANMAKU_SEGMENT_POLL_INTERVAL_MS = 15_000L\n    }\n\n    // 直播人气值与高能观众\n    var livePopularityText by mutableStateOf(\"\")   // \"2.5万人气\" (POPULARITY_CHANGE)\n    var liveOnlineCount by mutableStateOf(\"\")      // \"4333 高能观众\" (ONLINE_RANK_COUNT)\n\n    // 人气和高能观众更新频率限制（至少间隔 10 秒）\n    private var lastPopularityUpdateTime = 0L\n    private var lastOnlineCountUpdateTime = 0L\n\n    // 直播弹幕管理\n    private var liveWebSocket: Job? = null\n    private var liveWebSocketInner: Job? = null\n    private var liveDanmakuConsumer: Job? = null\n    private var liveDanmakuChannel: Channel<DanmakuEvent>? = null\n    private val liveDanmakuBuffer = mutableListOf<Danmaku>()\n    private var liveDanmakuFlushJob: Job? = null\n\n    // 点播弹幕管理\n    private var danmakuSegmentWatchJob: Job? = null\n    private var currentDanmakuSegmentIndex = -1\n\n    // 风控 Geetest 验证状态\n    var showGeetestDialog by mutableStateOf(false)\n    var geetestGt by mutableStateOf(\"\")\n    var geetestChallenge by mutableStateOf(\"\")\n    private var pendingGaiaToken: String? = null\n    private val loadedDanmakuSegmentCounts = mutableMapOf<Int, Int>()\n    var currentLoadedDanmakuTotal by mutableIntStateOf(0)\n\n    var coin by mutableStateOf(0)\n    var favorite by mutableStateOf(0)\n    var upName by mutableStateOf(\"\")\n    var upFace by mutableStateOf(\"\")\n    var pubTime by mutableStateOf(\"\")\n    var upId by mutableLongStateOf(0L)\n    var showDanmaku by mutableStateOf(Prefs.showDanmaku)\n    var showRelatedVideos by mutableStateOf(false)\n    var isFollowingUp by mutableStateOf(false)\n\n    var needPay by mutableStateOf(false)\n    var showPreviewTip by mutableStateOf(false)\n\n    var logs by mutableStateOf(\"\")\n    var lastChangedLog by mutableLongStateOf(System.currentTimeMillis())\n    var showBuffering by mutableStateOf(false)\n\n    var playerIconIdle by mutableStateOf(\"\")\n    var playerIconMoving by mutableStateOf(\"\")\n\n    var lastVideoHost by mutableStateOf(\"\")\n    var lastAudioHost by mutableStateOf(\"\")\n\n    var currentAid = 0L\n    var currentCid by mutableLongStateOf(0L)\n    private var currentEpid = 0\n\n    private suspend fun ensureDanmakuView() {\n        // DanmakuView is created by the UI layer, nothing to do here.\n        // Kept for call-site compatibility.\n        logger.fInfo { \"ensureDanmakuView: current=$danmakuView\" }\n    }\n\n    private fun stopDanmakuSegmentLoading() {\n        danmakuSegmentWatchJob?.cancel()\n        danmakuSegmentWatchJob = null\n        currentDanmakuSegmentIndex = -1\n        loadedDanmakuSegmentCounts.clear()\n        currentLoadedDanmakuTotal = 0\n    }\n\n    private fun getDanmakuSegmentIndex(positionMs: Long): Int {\n        return (positionMs / DANMAKU_SEGMENT_DURATION_MS).toInt() + 1\n    }\n\n    private suspend fun loadDanmakuSegment(cid: Long, positionMs: Long, force: Boolean = false) {\n        val safePosition = positionMs.coerceAtLeast(0L)\n        val segmentIndex = getDanmakuSegmentIndex(safePosition + DANMAKU_SEGMENT_POLL_INTERVAL_MS)\n        if (!force && loadedDanmakuSegmentCounts.containsKey(segmentIndex)) {\n            currentDanmakuSegmentIndex = segmentIndex\n            return\n        }\n\n        loadedDanmakuSegmentCounts[segmentIndex] = 0\n        var loadedCount = 0\n        runCatching {\n            val segmentData = BiliHttpApi.getDanmakuSeg(\n                cid = cid,\n                avid = currentAid,\n                segmentIndex = segmentIndex,\n                sessData = Prefs.sessData\n            )\n\n            val convertedDanmaku = segmentData.map {\n                Danmaku(\n                    dmid = it.dmid,\n                    positionMs = (it.time * 1000).toInt(),\n                    text = it.text,\n                    mode = it.type,\n                    textSize = it.size,\n                    color = 0xFF000000.toInt() or (it.color and 0xFFFFFF),\n                    level = it.level\n                )\n            }.sortedWith(compareBy({ it.positionMs }, { it.level }))\n            loadedCount = convertedDanmaku.size\n\n            val shouldResume = withContext(Dispatchers.Main) {\n                videoPlayer?.isPlaying == true\n            }\n\n            withContext(Dispatchers.Main) {\n                danmakuView?.appendDanmakus(convertedDanmaku, maxItems = 0, alreadySorted = true)\n            }\n\n            currentDanmakuSegmentIndex = segmentIndex\n            loadedDanmakuSegmentCounts[segmentIndex] = loadedCount\n            currentLoadedDanmakuTotal = loadedDanmakuSegmentCounts.values.sum()\n        }.onFailure {\n            loadedDanmakuSegmentCounts.remove(segmentIndex)\n            addLogs(\"加载第 $segmentIndex 块弹幕失败：${it.localizedMessage}\")\n            logger.fWarn { \"Load danmaku segment failed: cid=$cid, segment=$segmentIndex, error=${it.stackTraceToString()}\" }\n        }.onSuccess {\n            // 已加载 x 块 x 条弹幕（新追加的第 y 块有 z 条）\n            addLogs(\"已加载 ${loadedDanmakuSegmentCounts.size} 块共 $currentLoadedDanmakuTotal 条弹幕（6分钟/块）\", replaceIfContains = \"已加载\")\n            logger.fInfo { \"Load danmaku segment success, cid=$cid, segment=$segmentIndex, size=$loadedCount, total=$currentLoadedDanmakuTotal\" }\n        }\n    }\n\n    private fun startDanmakuSegmentWatcher(cid: Long) {\n        danmakuSegmentWatchJob?.cancel()\n        danmakuSegmentWatchJob = viewModelScope.launch(Dispatchers.Default) {\n            while (isActive && currentCid == cid && !isLive) {\n                val position = withContext(Dispatchers.Main) {\n                    videoPlayer?.currentPosition?.coerceAtLeast(0L) ?: 0L\n                }\n                loadDanmakuSegment(cid, position)\n                delay(DANMAKU_SEGMENT_POLL_INTERVAL_MS)\n            }\n        }\n    }\n\n    fun onVideoSeeked(positionMs: Long) {\n        if (isLive || currentCid <= 0L) return\n\n        viewModelScope.launch(Dispatchers.Default) {\n            loadDanmakuSegment(currentCid, positionMs)\n        }\n    }\n\n    fun loadPlayUrl(\n        avid: Long,\n        cid: Long,\n        epid: Int? = null,\n        seasonId: Int? = null,\n        continuePlayNext: Boolean = false\n    ) {\n        currentAid = avid\n        currentCid = cid\n        currentEpid = epid ?: 0\n        epid?.let { this.epid = it }\n        seasonId?.let { this.seasonId = it }\n        if (fromSeason && currentPlayMode in listOf(PlayMode.ListOrder, PlayMode.ListOrderReverse, PlayMode.RelatedVideo)) {\n            currentPlayMode = PlayMode.PartAndEpisode\n        }\n        if (!fromSeason) {\n            if (currentPlayMode in listOf(PlayMode.ListOrder, PlayMode.ListOrderReverse) && preloadedVideoList.isEmpty()) {\n                currentPlayMode = PlayMode.PartAndEpisode\n            }\n            if (currentPlayMode == PlayMode.RelatedVideo && relatedVideos.isEmpty()) {\n                currentPlayMode = PlayMode.PartAndEpisode\n            }\n        }\n        cancelPlayUrlAutoRefresh(\"new_media\")\n        viewModelScope.launch(Dispatchers.Default) {\n            // addLogs(\"加载视频中\")\n            ensureDanmakuView()\n            // addLogs(\"弹幕引擎已就绪\")\n            if (epid != null || seasonId != null) {\n                addLogs(\"avid:$avid，cid:$cid，epid:$epid，seasonId:$seasonId\")\n            } else {\n                addLogs(\"avid:$avid，cid:$cid\")\n            }\n\n            val lastPlayEnabledSubtitle = currentSubtitleId != -1L\n            val lastSubtitleLang = availableSubtitle.find { it.id == currentSubtitleId }?.langDoc\n            if (lastPlayEnabledSubtitle) {\n                logger.info { \"Subtitle is enabled, next video will enable subtitle automatic\" }\n            }\n\n            updateSubtitle()\n            loadPlayUrl(avid, cid, epid ?: 0, preferApi = Prefs.apiType, proxyArea = proxyArea)\n            // addLogs(\"加载弹幕中\")\n            loadDanmaku(cid)\n            updateDanmakuMask()\n\n            updateVideoShot()\n\n            //如果是继续播放下一集，且之前开启了字幕，就自动加载之前选择的语言的字幕\n            //否则根据默认字幕设置自动加载\n            if (continuePlayNext && lastPlayEnabledSubtitle) {\n                autoLoadSubtitle(preferLang = lastSubtitleLang, ccOnly = false)\n            } else if (!continuePlayNext) {\n                autoLoadSubtitle(preferLang = null, ccOnly = true)\n            }\n        }\n    }\n\n    private suspend fun loadPlayUrl(\n        avid: Long,\n        cid: Long,\n        epid: Int = 0,\n        preferApi: ApiType = Prefs.apiType,\n        proxyArea: ProxyArea = ProxyArea.MainLand\n    ) {\n        logger.fInfo { \"Load play url: [av=$avid, cid=$cid, preferApi=$preferApi, proxyArea=$proxyArea]\" }\n        withContext(Dispatchers.Main) { loadState = RequestState.Ready }\n        logger.fInfo { \"Set request state: ready\" }\n        logger.fInfo { \"fromSeason: $fromSeason\" }\n        runCatching {\n            val playData = if (fromSeason) {\n                videoPlayRepository.getPgcPlayData(\n                    aid = avid,\n                    cid = cid,\n                    epid = epid,\n                    preferCodec = Prefs.defaultVideoCodec.toBiliApiCodeType(),\n                    preferApiType = Prefs.apiType,\n                    enableProxy = Prefs.enableProxy,\n                    proxyArea = when (proxyArea) {\n                        ProxyArea.MainLand -> \"\"\n                        ProxyArea.HongKong -> \"hk\"\n                        ProxyArea.TaiWan -> \"tw\"\n                    }\n                )\n            } else {\n                videoPlayRepository.getPlayData(\n                    aid = avid,\n                    cid = cid,\n                    preferApiType = Prefs.apiType\n                )\n            }\n\n            //检查是否需要购买/充电，如果是试看则继续播放试看片段\n            withContext(Dispatchers.Main) { needPay = playData.needPay }\n\n            withContext(Dispatchers.Main) { this@VideoPlayerV3ViewModel.playData = playData }\n            withContext(Dispatchers.Main) { this@VideoPlayerV3ViewModel.clipInfoList = playData.clipInfoList }\n            logger.fInfo { \"Load play data response success\" }\n            //logger.info { \"Play data: $playData\" }\n\n            //读取清晰度\n            val resolutionList = mutableListOf<Resolution>()\n            playData.dashVideos.forEach {\n                Resolution.fromCode(it.quality)?.let { resolution ->\n                    if (!resolutionList.contains(resolution)) resolutionList.add(resolution)\n                }\n            }\n\n            logger.fInfo { \"Video available resolution: $resolutionList\" }\n            availableQuality.swapListWithMainContext(resolutionList)\n\n            //读取音频\n            val audioList = mutableListOf<Audio>()\n            playData.dashAudios.forEach {\n                Audio.fromCode(it.codecId)?.let { audio ->\n                    if (!audioList.contains(audio)) audioList.add(audio)\n                }\n            }\n            playData.dolby?.let {\n                Audio.fromCode(it.codecId)?.let { audio ->\n                    audioList.add(audio)\n                }\n            }\n            playData.flac?.let {\n                Audio.fromCode(it.codecId)?.let { audio ->\n                    audioList.add(audio)\n                }\n            }\n\n            logger.fInfo { \"Video available audio: $audioList\" }\n            availableAudio.swapListWithMainContext(audioList)\n\n            // 确定使用哪个默认分辨率\n            val defaultQualityToUse = if (\n                isVerticalVideo &&\n                Prefs.portraitVideoFixMode == PortraitVideoFixMode.LimitResolution1080P &&\n                Prefs.defaultQuality >= Resolution.R4K\n            ) {\n                // 如果是竖屏视频且用户设置了竖屏视频限制最高使用1080P\n                Resolution.R1080P60\n            } else {\n                // 否则使用普通设置\n                Prefs.defaultQuality\n            }\n\n            //先确认最终所选清晰度\n            val existDefaultResolution =\n                availableQuality.find { it == defaultQualityToUse } != null\n\n            if (!existDefaultResolution) {\n                val tempList = resolutionList.sortedByDescending { it.code }\n                val currentQuality = tempList.firstOrNull { it.code < defaultQualityToUse.code }\n                    ?: tempList.last()\n                withContext(Dispatchers.Main) {\n                    this@VideoPlayerV3ViewModel.currentQuality = currentQuality\n                }\n            } else {\n                // 如果默认清晰度可用，直接使用\n                withContext(Dispatchers.Main) { currentQuality = defaultQualityToUse }\n            }\n\n            //确认最终所选音质\n            val existDefaultAudio = availableAudio.contains(Prefs.defaultAudio)\n            if (!existDefaultAudio && availableAudio.isNotEmpty()) {\n                val currentAudio = when {\n                    Prefs.defaultAudio == Audio.ADolbyAtoms && availableAudio.contains(Audio.ADolbyAtoms) -> Audio.ADolbyAtoms\n                    (Prefs.defaultAudio == Audio.ADolbyAtoms || Prefs.defaultAudio == Audio.AHiRes) && availableAudio.contains(Audio.AHiRes) -> Audio.AHiRes\n                    availableAudio.contains(Audio.A192K) -> Audio.A192K\n                    availableAudio.contains(Audio.A132K) -> Audio.A132K\n                    availableAudio.contains(Audio.A64K) -> Audio.A64K\n                    else -> availableAudio.first()\n                }\n                withContext(Dispatchers.Main) {\n                    this@VideoPlayerV3ViewModel.currentAudio = currentAudio\n                }\n            }\n\n            //再确认最终所选视频编码\n            updateAvailableCodec()\n\n            playQuality(qn = currentQuality.code, codec = currentVideoCodec)\n\n            // 充电/付费视频预览状态提示\n            if (playData.needPay) {\n                startShowPreviewTipCountdown()\n            }\n\n        }.onFailure {\n            if (it is VVoucherException) {\n                logger.fWarn { \"Risk control v_voucher detected: ${it.vVoucher}\" }\n                addLogs(\"触发风控，正在申请验证…\")\n                handleVVoucher(it.vVoucher)\n                return@onFailure\n            }\n            addLogs(\"加载视频地址失败：${it.localizedMessage}\")\n            errorMessage = it.localizedMessage ?: \"Unknown error\"\n            loadState = RequestState.Failed\n            logger.fException(it) { \"Load video failed\" }\n        }.onSuccess {\n            // addLogs(\"加载视频地址成功\")\n            loadState = RequestState.Success\n            logger.fInfo { \"Load play url success\" }\n        }\n    }\n\n    private suspend fun handleVVoucher(vVoucher: String) {\n        runCatching {\n            val registerResponse = BiliHttpApi.gaiaVgateRegister(\n                vVoucher = vVoucher,\n                sessData = authRepository.sessionData,\n                csrf = authRepository.biliJct\n            ).getResponseData()\n            val token = registerResponse.token\n            val gt = registerResponse.geetest.gt\n            val challenge = registerResponse.geetest.challenge\n            if (token.isBlank() || gt.isBlank() || challenge.isBlank()) {\n                error(\"gaia_vgate_register 返回数据不完整\")\n            }\n            withContext(Dispatchers.Main) {\n                pendingGaiaToken = token\n                geetestGt = gt\n                geetestChallenge = challenge\n                showGeetestDialog = true\n            }\n            addLogs(\"请完成人机验证\")\n        }.onFailure {\n            addLogs(\"风控验证申请失败：${it.localizedMessage}\")\n            withContext(Dispatchers.Main) {\n                errorMessage = \"风控验证申请失败：${it.localizedMessage}\"\n                loadState = RequestState.Failed\n            }\n            logger.fException(it) { \"gaiaVgateRegister failed\" }\n        }\n    }\n\n    fun onGeetestResult(challenge: String, validate: String, seccode: String) {\n        val token = pendingGaiaToken ?: return\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                // addLogs(\"正在提交验证结果…\")\n                val validateResponse = BiliHttpApi.gaiaVgateValidate(\n                    token = token,\n                    geetestChallenge = challenge,\n                    validate = validate,\n                    seccode = seccode,\n                    sessData = authRepository.sessionData,\n                    csrf = authRepository.biliJct\n                ).getResponseData()\n                if (validateResponse.isValid != 1) {\n                    error(\"验证未通过\")\n                }\n                val griskId = validateResponse.griskId\n                if (griskId.isBlank()) {\n                    error(\"grisk_id 为空\")\n                }\n                authRepository.gaiaVtoken = griskId\n                withContext(Dispatchers.Main) {\n                    showGeetestDialog = false\n                    pendingGaiaToken = null\n                }\n                addLogs(\"风控验证通过\")\n                logger.fInfo { \"Gaia vgate validate success, retrying play url\" }\n                loadPlayUrl(currentAid, currentCid, currentEpid, preferApi = Prefs.apiType, proxyArea = proxyArea)\n            }.onFailure {\n                addLogs(\"风控验证失败：${it.localizedMessage}\")\n                withContext(Dispatchers.Main) {\n                    errorMessage = \"风控验证失败：${it.localizedMessage}\"\n                    loadState = RequestState.Failed\n                    showGeetestDialog = false\n                    pendingGaiaToken = null\n                }\n                logger.fException(it) { \"gaiaVgateValidate failed\" }\n            }\n        }\n    }\n\n    fun onGeetestCancelled() {\n        showGeetestDialog = false\n        pendingGaiaToken = null\n        errorMessage = \"验证已取消\"\n        loadState = RequestState.Failed\n        viewModelScope.launch {\n            addLogs(\"用户取消了风控验证\")\n        }\n    }\n\n    private suspend fun updateAvailableCodec() {\n        if (Prefs.apiType == ApiType.App && playData!!.codec.isEmpty()) {\n            // 纠正当前实际播放的编码\n            val videoItem = playData!!.dashVideos\n                .find { it.quality == currentQuality.code }\n                ?: playData!!.dashVideos.first()\n            withContext(Dispatchers.Main) {\n                currentVideoCodec = VideoCodec.fromCodecId(videoItem.codecId)\n            }\n            logger.fInfo { \"App API fixed, Select codec: $currentVideoCodec\" }\n            return\n        }\n\n        val supportedCodec = playData!!.codec\n        val codecList =\n            supportedCodec[currentQuality.code]?.mapNotNull { VideoCodec.fromCodecString(it) } ?: emptyList()\n\n        availableVideoCodec.swapListWithMainContext(codecList)\n        logger.fInfo { \"Video available codec: ${availableVideoCodec.toList()}\" }\n\n        logger.fInfo { \"Default codec: $currentVideoCodec\" }\n        val currentVideoCodec = if (codecList.contains(this@VideoPlayerV3ViewModel.currentVideoCodec)) {\n            this@VideoPlayerV3ViewModel.currentVideoCodec\n        } else if (codecList.contains(Prefs.defaultVideoCodec)) {\n            Prefs.defaultVideoCodec\n        } else {\n            codecList.minByOrNull { it.ordinal }!!\n        }\n        withContext(Dispatchers.Main) {\n            this@VideoPlayerV3ViewModel.currentVideoCodec = currentVideoCodec\n        }\n        logger.fInfo { \"Select codec: $currentVideoCodec\" }\n    }\n\n    /**\n     * 解码器错误时自动降级到更低清晰度\n     * @return true 表示已成功降级，false 表示已是最低清晰度无法降级\n     */\n    private fun fallbackToLowerQuality(): Boolean {\n        val sortedQualities = availableQuality.sortedByDescending { it.code }\n        val lowerQuality = sortedQualities.firstOrNull { it.code < currentQuality.code }\n            ?: return false\n        logger.fInfo { \"Decoder error, fallback from $currentQuality to $lowerQuality\" }\n        viewModelScope.launch(Dispatchers.Main) {\n            val position = videoPlayer?.currentPosition ?: 0\n            playQuality(qn = lowerQuality)\n            if (position > 0) videoPlayer?.seekTo(position)\n            videoPlayer?.start()\n        }\n        return true\n    }\n\n    suspend fun playQuality(\n        qn: Resolution = currentQuality,\n        codec: VideoCodec = currentVideoCodec,\n        audio: Audio = currentAudio\n    ) {\n        if (qn != currentQuality) {\n            // 更新清晰度后需要先设置清晰度再更新编码列表\n            withContext(Dispatchers.Main) { currentQuality = qn }\n            updateAvailableCodec()\n            playQuality(qn.code, currentVideoCodec, audio)\n        } else {\n            playQuality(qn.code, codec, audio)\n        }\n    }\n\n    private suspend fun playQuality(\n        qn: Int = currentQuality.code,\n        codec: VideoCodec = currentVideoCodec,\n        audio: Audio = currentAudio\n    ) {\n        logger.fInfo { \"Select resolution: $qn, codec: $codec, audio: $audio\" }\n        if(playData == null) {\n            return\n        }\n\n        val videoItem = playData!!.dashVideos.find {\n            when (Prefs.apiType) {\n                ApiType.Web -> it.quality == qn && it.codecs?.startsWith(codec.prefix) == true\n                ApiType.App -> {\n                    if (playData!!.codec.isEmpty()) it.quality == qn\n                    else it.quality == qn && it.codecs?.startsWith(codec.prefix) == true\n                }\n            }\n        } ?: playData!!.dashVideos.firstOrNull()\n        var videoUrl = videoItem?.baseUrl\n        if (videoUrl == null) {\n            logger.fError { \"Failed to get video URL\" }\n            errorMessage = \"获取视频地址失败\"\n            loadState = RequestState.Failed\n            return\n        }\n        val videoUrls = mutableListOf<String?>()\n        videoUrls.add(videoItem?.baseUrl)\n        videoUrls.addAll(videoItem?.backUrl ?: emptyList())\n\n        val audioItem = listOfNotNull(\n            playData!!.dashAudios.find { it.codecId == audio.code },\n            playData!!.dolby.takeIf { it?.codecId == audio.code },\n            playData!!.flac.takeIf { it?.codecId == audio.code },\n            playData!!.dashAudios.minByOrNull { it.codecId },\n            playData!!.dolby,\n            playData!!.flac\n        ).firstOrNull()\n        var audioUrl = audioItem?.baseUrl\n        val audioUrls = mutableListOf<String>()\n        audioItem?.baseUrl?.let(audioUrls::add)\n        audioUrls.addAll(audioItem?.backUrl ?: emptyList())\n\n        logger.fInfo { \"all video hosts: ${videoUrls.filterNotNull().map { with(URI(it)) { \"$scheme://$authority\" } }}\" }\n        logger.fInfo { \"all audio hosts: ${audioUrls.map { with(URI(it)) { \"$scheme://$authority\" } }}\" }\n\n        //replace cdn\n        if (Prefs.enableProxy && proxyArea != ProxyArea.MainLand) {\n            videoUrl = videoUrl.replaceUrlDomainWithAliCdn()\n            audioUrl = audioUrl?.replaceUrlDomainWithAliCdn()\n        } else {\n            // 如果未通过网络代理获得播放地址，才判断是否应该替换为官方 cdn\n            videoUrl = selectOfficialCdnUrl(videoUrls.filterNotNull())\n            audioUrl = audioUrls.takeIf { it.isNotEmpty() }?.let(::selectOfficialCdnUrl)\n        }\n\n        if (audioUrl == null) {\n            logger.fWarn { \"Failed to get audio URL, fallback to video-only playback\" }\n        }\n\n        addLogs(\n            \"播放清晰度：${availableQuality.firstOrNull { it.code == qn }}, \" +\n                    \"视频编码：${codec.getDisplayName(BVApp.context)}, \" +\n                    \"音频编码：${(Audio.fromCode(audioItem?.codecId ?: 0))?.getDisplayName(BVApp.context) ?: \"未知\"}\",\n            replaceIfContains = \"播放清晰度\"\n        )\n\n        var videoHost = with(URI(videoUrl)) { \"$scheme://$authority\" }\n        var audioHost = audioUrl?.let { with(URI(it)) { \"$scheme://$authority\" } } ?: \"无音频流，使用纯视频播放\"\n        addLogs(\"video host: $videoHost\", replaceIfContains = \"video host\")\n        addLogs(\"audio host: $audioHost\", replaceIfContains = \"audio host\")\n\n        logger.fInfo { \"Select audio: $audioItem\" }\n\n        withContext(Dispatchers.Main) {\n            currentVideoHeight = videoItem?.height ?: 0\n            currentVideoWidth = videoItem?.width ?: 0\n            lastVideoHost = videoHost\n            lastAudioHost = audioHost\n            logger.info { \"Video url: $videoUrl\" }\n            logger.info { \"Audio url: $audioUrl\" }\n            videoPlayer!!.playUrl(videoUrl, audioUrl)\n            // 根据 DefaultStartPosition 设置初始跳转位置，避免在 onReady 中 seekTo 导致的状态抖动\n            if (lastPlayed > 0 && Prefs.playerDefaultStartPosition == PlayerDefaultStartPosition.History) {\n                logger.info { \"Set initial seek position to history: ${lastPlayed}ms\" }\n                videoPlayer!!.setInitialSeekPosition(lastPlayed.toLong())\n            }\n            videoPlayer!!.prepare()\n            showBuffering = true\n        }\n        // 为点播内容安排CDN URL自动刷新，防止2小时有效期过期\n        if (!isLive) {\n            schedulePlayUrlAutoRefresh(\n                videoUrl = videoUrl,\n                audioUrl = audioUrl,\n                reason = \"play_quality\"\n            )\n        }\n    }\n\n    private fun startShowPreviewTipCountdown() {\n        previewTipJob?.cancel()\n        previewTipJob = viewModelScope.launch {\n            showPreviewTip = true\n            delay(5000)\n            showPreviewTip = false\n        }\n    }\n\n    // ==================== 点播CDN URL自动刷新 ====================\n\n    private fun cancelPlayUrlAutoRefresh(reason: String) {\n        playUrlAutoRefreshJob?.cancel()\n        playUrlAutoRefreshJob = null\n        playUrlAutoRefreshToken++\n        logger.fInfo { \"playurl:autoRefresh:cancel reason=$reason\" }\n    }\n\n    private fun schedulePlayUrlAutoRefresh(\n        videoUrl: String,\n        audioUrl: String?,\n        reason: String\n    ) {\n        playUrlAutoRefreshJob?.cancel()\n        playUrlAutoRefreshJob = null\n\n        val nowWallMs = System.currentTimeMillis()\n        val deadlineEpochSec = pickEarliestDeadlineEpochSec(videoUrl, audioUrl)\n\n        val delayMs = if (deadlineEpochSec != null) {\n            val refreshWallMs = deadlineEpochSec * 1000L - PLAYURL_AUTO_REFRESH_LEAD_MS\n            (refreshWallMs - nowWallMs).coerceAtLeast(0L)\n        } else {\n            val durationMs = videoPlayer?.duration?.takeIf { it > 0 }\n            if (durationMs != null && durationMs >= PLAYURL_AUTO_REFRESH_FALLBACK_MIN_DURATION_MS) {\n                PLAYURL_AUTO_REFRESH_FALLBACK_DELAY_MS\n            } else {\n                logger.fInfo {\n                    \"playurl:autoRefresh:skip reason=$reason deadline=none duration=${durationMs ?: -1}ms\"\n                }\n                return\n            }\n        }\n\n        val token = ++playUrlAutoRefreshToken\n        val aid = currentAid\n        val cid = currentCid\n\n        logger.fInfo {\n            \"playurl:autoRefresh:schedule delay=${delayMs}ms deadline=${deadlineEpochSec ?: -1} reason=$reason\"\n        }\n\n        playUrlAutoRefreshJob = viewModelScope.launch(Dispatchers.Main) {\n            delay(delayMs)\n            if (token != playUrlAutoRefreshToken) return@launch\n            if (videoPlayer == null) return@launch\n            if (currentAid != aid || currentCid != cid) return@launch\n\n            logger.fInfo {\n                \"playurl:autoRefresh:reload token=$token pos=${videoPlayer?.currentPosition ?: 0}ms\"\n            }\n\n            // 重新加载播放URL（保持当前位置）\n            reloadPlayUrl()\n        }\n    }\n\n    /**\n     * 重新加载播放URL，保持当前播放位置和状态\n     * 用于CDN URL过期前的主动刷新\n     */\n    private suspend fun reloadPlayUrl() {\n        val aid = currentAid\n        val cid = currentCid\n        val currentPos = withContext(Dispatchers.Main) {\n            videoPlayer?.currentPosition?.coerceAtLeast(0L) ?: 0L\n        }\n        val wasPlaying = withContext(Dispatchers.Main) {\n            videoPlayer?.isPlaying == true\n        }\n\n        logger.fInfo { \"reloadPlayUrl: aid=$aid, cid=$cid, pos=${currentPos}ms\" }\n\n        runCatching {\n            val playData = if (fromSeason) {\n                videoPlayRepository.getPgcPlayData(\n                    aid = aid,\n                    cid = cid,\n                    epid = currentEpid,\n                    preferCodec = Prefs.defaultVideoCodec.toBiliApiCodeType(),\n                    preferApiType = Prefs.apiType,\n                    enableProxy = Prefs.enableProxy,\n                    proxyArea = when (proxyArea) {\n                        ProxyArea.MainLand -> \"\"\n                        ProxyArea.HongKong -> \"hk\"\n                        ProxyArea.TaiWan -> \"tw\"\n                    }\n                )\n            } else {\n                videoPlayRepository.getPlayData(\n                    aid = aid,\n                    cid = cid,\n                    preferApiType = Prefs.apiType\n                )\n            }\n\n            withContext(Dispatchers.Main) { this@VideoPlayerV3ViewModel.playData = playData }\n\n            // 使用当前清晰度和编码重新播放\n            val qn = currentQuality\n            val codec = currentVideoCodec\n            val audio = currentAudio\n\n            // 查找视频项\n            val videoItem = playData.dashVideos.find {\n                when (Prefs.apiType) {\n                    ApiType.Web -> it.quality == qn.code && it.codecs!!.startsWith(codec.prefix)\n                    ApiType.App -> {\n                        if (playData.codec.isEmpty()) it.quality == qn.code\n                        else it.quality == qn.code && it.codecs!!.startsWith(codec.prefix)\n                    }\n                }\n            }\n            var videoUrl = videoItem?.baseUrl ?: playData.dashVideos.firstOrNull()?.baseUrl\n                ?: return@runCatching\n\n            val videoUrls = mutableListOf<String?>()\n            videoUrls.add(videoItem?.baseUrl)\n            videoUrls.addAll(videoItem?.backUrl ?: emptyList())\n\n            val audioItem = listOfNotNull(\n                playData.dashAudios.find { it.codecId == audio.code },\n                playData.dolby.takeIf { it?.codecId == audio.code },\n                playData.flac.takeIf { it?.codecId == audio.code },\n                playData.dashAudios.minByOrNull { it.codecId },\n                playData.dolby,\n                playData.flac\n            ).firstOrNull()\n            var audioUrl = audioItem?.baseUrl\n            val audioUrls = mutableListOf<String>()\n            audioItem?.baseUrl?.let(audioUrls::add)\n            audioUrls.addAll(audioItem?.backUrl ?: emptyList())\n\n            if (Prefs.enableProxy && proxyArea != ProxyArea.MainLand) {\n                videoUrl = videoUrl.replaceUrlDomainWithAliCdn()\n                audioUrl = audioUrl?.replaceUrlDomainWithAliCdn()\n            } else {\n                videoUrl = selectOfficialCdnUrl(videoUrls.filterNotNull())\n                audioUrl = audioUrls.takeIf { it.isNotEmpty() }?.let(::selectOfficialCdnUrl)\n            }\n\n            withContext(Dispatchers.Main) {\n                videoPlayer?.let { player ->\n                    player.playUrl(videoUrl, audioUrl)\n                    player.prepare()\n                    player.seekTo(currentPos)\n                    if (wasPlaying) player.start()\n                }\n            }\n\n            // 安排下一次自动刷新\n            schedulePlayUrlAutoRefresh(\n                videoUrl = videoUrl,\n                audioUrl = audioUrl,\n                reason = \"auto_refresh\"\n            )\n\n            logger.fInfo { \"playurl:autoRefresh:reload:success\" }\n        }.onFailure {\n            logger.fException(it) { \"playurl:autoRefresh:reload:failed\" }\n        }\n    }\n\n    /**\n     * 从URL中解析CDN过期时间（epoch秒）\n     * B站CDN URL通常包含 deadline 或 expires 查询参数\n     */\n    private fun parseDeadlineEpochSec(url: String): Long? {\n        return runCatching {\n            val uri = Uri.parse(url)\n            val raw = uri.getQueryParameter(\"deadline\")\n                ?: uri.getQueryParameter(\"expires\")\n                ?: return null\n            raw.toLongOrNull()\n        }.getOrNull()\n    }\n\n    private fun pickEarliestDeadlineEpochSec(videoUrl: String, audioUrl: String?): Long? {\n        val videoDeadline = parseDeadlineEpochSec(videoUrl)\n        val audioDeadline = audioUrl?.let { parseDeadlineEpochSec(it) }\n        return when {\n            videoDeadline != null && audioDeadline != null -> minOf(videoDeadline, audioDeadline)\n            videoDeadline != null -> videoDeadline\n            audioDeadline != null -> audioDeadline\n            else -> null\n        }\n    }\n\n    private suspend fun <T> withPlayerOnMain(block: AbstractVideoPlayer.() -> T): T? {\n        return withContext(Dispatchers.Main) {\n            videoPlayer?.block()\n        }\n    }\n\n    suspend fun loadDanmaku(cid: Long) {\n        stopDanmakuSegmentLoading()\n\n        val initialPosition = if (\n            lastPlayed > 0 &&\n            Prefs.playerDefaultStartPosition == PlayerDefaultStartPosition.History\n        ) {\n            lastPlayed.toLong()\n        } else {\n            withContext(Dispatchers.Main) {\n                videoPlayer?.currentPosition?.coerceAtLeast(0L) ?: 0L\n            }\n        }\n\n        loadDanmakuSegment(cid, initialPosition, force = true)\n        startDanmakuSegmentWatcher(cid)\n    }\n\n    private suspend fun updateSubtitle() {\n        currentSubtitleId = -1\n        currentSubtitleData.clear()\n\n        runCatching {\n            val subtitleData = videoPlayRepository.getSubtitle(\n                aid = currentAid,\n                cid = currentCid,\n                preferApiType = Prefs.apiType\n            )\n            withContext(Dispatchers.Main) {\n                availableSubtitle.clear()\n                availableSubtitle.add(\n                    Subtitle(\n                        id = -1,\n                        lang = \"\",\n                        langDoc = \"关闭\",\n                        url = \"\",\n                        type = SubtitleType.CC,\n                        aiType = SubtitleAiType.Normal,\n                        aiStatus = SubtitleAiStatus.None\n                    )\n                )\n                availableSubtitle.addAll(subtitleData)\n                availableSubtitle.sortBy { it.id }\n            }\n            addLogs(\"获取到 ${subtitleData.size} 条字幕: ${subtitleData.map { it.langDoc }}\")\n            logger.fInfo { \"Update subtitle size: ${subtitleData.size}\" }\n        }.onFailure {\n            addLogs(\"获取字幕失败：${it.localizedMessage}\")\n            logger.fWarn { \"Update subtitle failed: ${it.stackTraceToString()}\" }\n        }\n    }\n\n    /**\n     * 自动加载字幕\n     *\n     * @param preferLang 优先匹配的语言名称（来自播放器中手动选择的字幕），优先级高于默认字幕设置\n     * @param ccOnly 是否仅匹配非AI字幕（type == CC）\n     */\n    private fun autoLoadSubtitle(preferLang: String?, ccOnly: Boolean) {\n        val defaultSubtitle = Prefs.defaultSubtitle\n        if (preferLang == null && defaultSubtitle == DefaultSubtitle.Off) return\n\n        val langKeyword = preferLang ?: when (defaultSubtitle) {\n            DefaultSubtitle.Chinese -> \"中文\"\n            DefaultSubtitle.English -> \"English\"\n            DefaultSubtitle.Off -> return\n        }\n\n        val candidates = availableSubtitle.filter { it.id != -1L && (!ccOnly || it.type == SubtitleType.CC) }\n        val match = candidates.firstOrNull { it.langDoc.contains(langKeyword, ignoreCase = true) }\n\n        if (match != null) {\n            logger.fInfo { \"Auto load subtitle: ${match.langDoc}\" }\n            loadSubtitle(match.id)\n        } else {\n            logger.fInfo { \"No matching subtitle for lang=\\\"$langKeyword\\\", ccOnly=$ccOnly\" }\n        }\n    }\n\n    private suspend fun addLogs(text: String, replaceIfContains: String? = null) {\n        logger.fInfo { text }\n        val lines = logs.lines().filter { it.isNotEmpty() }.toMutableList()\n        if (replaceIfContains != null) {\n            val idx = lines.indexOfLast { it.contains(replaceIfContains) }\n            if (idx >= 0) lines[idx] = text else lines.add(text)\n        } else {\n            lines.add(text)\n        }\n        while (lines.size > 8) {\n            lines.removeAt(0)\n        }\n        val newTip = lines.joinToString(\"\\n\")\n        withContext(Dispatchers.Main) {\n            logs = newTip\n            lastChangedLog = System.currentTimeMillis()\n            videoPlayer?.extraDebugInfo = newTip\n        }\n    }\n\n    suspend fun uploadHistory(time: Int) {\n        if (!Prefs.isLogin) {\n            return@uploadHistory\n        }\n        runCatching {\n            if (!fromSeason) {\n                logger.info { \"Send heartbeat: [avid=$currentAid, cid=$currentCid, time=$time]\" }\n                videoPlayRepository.sendHeartbeat(\n                    aid = currentAid,\n                    cid = currentCid,\n                    time = time,\n                    preferApiType = Prefs.apiType\n                )\n            } else {\n                logger.info { \"Send heartbeat: [avid=$currentAid, cid=$currentCid, epid=$epid, sid=$seasonId, time=$time]\" }\n                videoPlayRepository.sendHeartbeat(\n                    aid = currentAid,\n                    cid = currentCid,\n                    time = time,\n                    type = HeartbeatVideoType.Season,\n                    subType = subType,\n                    epid = epid,\n                    seasonId = seasonId,\n                    preferApiType = Prefs.apiType\n                )\n            }\n        }.onSuccess {\n            logger.info { \"Send heartbeat success\" }\n        }.onFailure {\n            logger.warn { \"Send heartbeat failed: ${it.stackTraceToString()}\" }\n        }\n    }\n\n    fun loadSubtitle(id: Long) {\n        viewModelScope.launch(Dispatchers.IO) {\n            if (id == -1L) {\n                withContext(Dispatchers.Main) {\n                    currentSubtitleData.clear()\n                    currentSubtitleId = -1\n                    currentSubtitleType = SubtitleType.CC\n                }\n                return@launch\n            }\n            var subtitleName = \"\"\n            runCatching {\n                val subtitle = availableSubtitle.find { it.id == id } ?: return@runCatching\n                subtitleName = subtitle.langDoc\n                val isAI = subtitle.type == SubtitleType.AI\n                logger.info { \"Subtitle url: ${subtitle.url}, isAI: $isAI\" }\n                val client = HttpClient(OkHttp)\n                val responseText = client.get(subtitle.url).bodyAsText()\n                val subtitleData = SubtitleParser.fromBccString(responseText, isAI)\n                withContext(Dispatchers.Main) {\n                    currentSubtitleId = id\n                    currentSubtitleType = subtitle.type\n                    currentSubtitleData.swapList(subtitleData)\n                }\n            }.onFailure {\n                withContext(Dispatchers.Main) {\n                    currentSubtitleData.clear()\n                    currentSubtitleId = -1\n                    currentSubtitleType = SubtitleType.CC\n                }\n                logger.fInfo { \"Load subtitle failed: ${it.stackTraceToString()}\" }\n                addLogs(\"加载字幕 $subtitleName 失败: ${it.localizedMessage}\")\n            }.onSuccess {\n                logger.fInfo { \"Load subtitle $subtitleName success\" }\n                addLogs(\"加载字幕 $subtitleName 成功，数量: ${currentSubtitleData.size}\")\n            }\n        }\n    }\n\n    private fun String.replaceUrlDomainWithAliCdn(): String {\n        val replaceDomainKeywords = listOf(\n            \"mirroraliov\",\n            \"mirrorakam\"\n        )\n        if (replaceDomainKeywords.none { this.contains(it) }) return this\n\n        return Uri.parse(this)\n            .buildUpon()\n            .authority(\"upos-sz-mirrorali.bilivideo.com\")\n            .build()\n            .toString()\n    }\n\n    private fun selectOfficialCdnUrl(urls: List<String>): String {\n        if (urls.isEmpty()) {\n            logger.fInfo { \"doesn't find any url, select a random url\" }\n            return urls.randomOrNull() ?: \"\"\n        }\n\n        // 判定是否为“官方” CDN 的简单规则，和之前逻辑保持一致\n        val isOfficialCdn: (String) -> Boolean = {\n            !it.contains(\".mcdn.bilivideo.\") &&\n            !it.contains(\".szbdyd.com\") &&\n            !Regex(\"^(https?://)?(\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}\\\\.\\\\d{1,3}(:\\\\d{1,5})?)(/[a-zA-Z0-9_./-]*)?(\\\\?.*)?$\")\n                .matches(it)\n        }\n\n        if (!Prefs.preferOfficialCdn) {\n            // 当用户不偏好官方 CDN 时，使用加权随机：官方权重 0.8，非官方权重 1.2（基准为 1）\n            logger.fInfo { \"doesn't need to filter official cdn url, select a weighted random url (favor non-official)\" }\n\n            val weights = urls.map { url -> if (isOfficialCdn(url)) 1 else 1 }\n            val total = weights.sum()\n            // 如果权重计算异常，退回随机\n            if (total <= 0.0) return urls.randomOrNull() ?: \"\"\n\n            val r = kotlin.random.Random.Default.nextDouble() * total\n            var acc = 0.0\n            for (i in urls.indices) {\n                acc += weights[i]\n                if (r <= acc) return urls[i]\n            }\n            return urls.randomOrNull() ?: \"\"\n        }\n\n        val filteredUrls = urls.filter{isOfficialCdn(it)}\n        if (filteredUrls.isEmpty()) {\n            logger.fInfo { \"doesn't find any official cdn url, select a random url\" }\n            return urls.random()\n        } else {\n            logger.fInfo { \"filtered official cdn urls: $filteredUrls\" }\n            return filteredUrls.random()\n        }\n    }\n\n    private suspend fun updateDanmakuMask() {\n        runCatching {\n            val masks = videoPlayRepository.getDanmakuMask(\n                aid = currentAid,\n                cid = currentCid,\n                preferApiType = Prefs.apiType\n            )\n            danmakuMasks.swapListWithMainContext(masks)\n            logger.fInfo { \"Load danmaku mask size: ${danmakuMasks.size}\" }\n        }.onFailure {\n            logger.fWarn { \"Load danmaku mask failed: ${it.stackTraceToString()}\" }\n        }\n    }\n\n    private suspend fun updateVideoShot() {\n        withContext(Dispatchers.Main) { videoShot = null }\n        runCatching {\n            val videoShot = videoPlayRepository.getVideoShot(\n                aid = currentAid,\n                cid = currentCid,\n                preferApiType = Prefs.apiType\n            )\n            withContext(Dispatchers.Main) { this@VideoPlayerV3ViewModel.videoShot = videoShot }\n            logger.fInfo { \"Load video shot success\" }\n        }.onFailure {\n            logger.fWarn { \"Load video shot failed: ${it.stackTraceToString()}\" }\n        }\n    }\n\n    // 这个方法当时只适配了移动端逻辑，TV端比较复杂，另外写了一份\n    fun playNextVideo() {\n        logger.fInfo { \"Video finished\" }\n        when (currentPlayMode) {\n            PlayMode.Custom -> {\n                logger.info { \"Play mode: $currentPlayMode, using strategy order\" }\n                val validOrdinals = NextVideoStrategy.entries.map { it.ordinalValue }.toSet()\n                val strategies = Prefs.playerNextVideoStrategyOrder.split(\",\")\n                    .filter { !it.startsWith(\"-\") }\n                    .mapNotNull {\n                        val id = it.toIntOrNull() ?: return@mapNotNull null\n                        if (id !in validOrdinals) return@mapNotNull null\n                        NextVideoStrategy.fromOrdinal(id)\n                    }\n                for (strategy in strategies) {\n                    when (strategy) {\n                        NextVideoStrategy.SingleVideo -> {\n                            logger.info { \"Strategy SingleVideo: stop\" }\n                            return\n                        }\n                        NextVideoStrategy.PartAndEpisode,\n                        NextVideoStrategy.PreloadedVideoList -> {\n                            if (playNextVideoInList()) return\n                        }\n                        NextVideoStrategy.PartAndEpisodeReverse,\n                        NextVideoStrategy.PreloadedVideoListReverse -> {\n                            if (playPrevVideoInList()) return\n                        }\n                        NextVideoStrategy.RelatedVideo -> {\n                            // handled by screen\n                            logger.info { \"Strategy RelatedVideo: handled by screen\" }\n                        }\n                    }\n                }\n            }\n\n            PlayMode.SingleVideo -> {\n                logger.info { \"Play mode: $currentPlayMode, no auto next\" }\n            }\n\n            PlayMode.SingleLoop -> {\n                logger.info { \"Play mode: $currentPlayMode, replay current video\" }\n                danmakuView?.notifySeek(0L)\n                videoPlayer?.seekTo(0L)\n            }\n\n            PlayMode.ListOrder -> {\n                logger.info { \"Play mode: $currentPlayMode, play next video in list\" }\n                playNextVideoInList()\n            }\n\n            PlayMode.ListOrderReverse -> {\n                logger.info { \"Play mode: $currentPlayMode, play previous video in list\" }\n                playPrevVideoInList()\n            }\n\n            PlayMode.PartAndEpisode -> {\n                logger.info { \"Play mode: $currentPlayMode, play next video in list\" }\n                playNextVideoInList()\n            }\n\n            PlayMode.PartAndEpisodeReverse -> {\n                logger.info { \"Play mode: $currentPlayMode, play previous video in list\" }\n                playPrevVideoInList()\n            }\n\n            PlayMode.RelatedVideo -> {\n                logger.info { \"Play mode: $currentPlayMode, do nothing (handled by screen)\" }\n            }\n        }\n    }\n\n    private fun playNextVideoInList(loop: Boolean = false): Boolean {\n        val currentIndex = availableVideoList\n            .indexOfFirst {\n                when (it) {\n                    is VideoListItemData -> it.cid == currentCid\n                    else -> false\n                }\n            }\n        if (currentIndex + 1 < availableVideoList.size) {\n            val nextVideos = availableVideoList.subList(\n                currentIndex + 1,\n                availableVideoList.size\n            )\n            val nextVideo =\n                nextVideos.firstOrNull { it is VideoListItemData }!! as VideoListItemData\n            logger.info { \"Play next video: $nextVideo\" }\n            partTitle = nextVideo.title\n            loadPlayUrl(\n                avid = nextVideo.aid,\n                cid = nextVideo.cid!!,\n                epid = nextVideo.epid,\n                seasonId = nextVideo.seasonId,\n                continuePlayNext = true\n            )\n            return true\n        } else if (loop) {\n            //loop to first\n            val firstVideo =\n                availableVideoList.firstOrNull { it is VideoListItemData }!! as VideoListItemData\n            logger.info { \"Loop to first video: $firstVideo\" }\n            partTitle = firstVideo.title\n            loadPlayUrl(\n                avid = firstVideo.aid,\n                cid = firstVideo.cid!!,\n                epid = firstVideo.epid,\n                seasonId = firstVideo.seasonId,\n                continuePlayNext = true\n            )\n            return true\n        }\n        return false\n    }\n\n    private fun playPrevVideoInList(): Boolean {\n        val currentIndex = availableVideoList\n            .indexOfFirst {\n                when (it) {\n                    is VideoListItemData -> it.cid == currentCid\n                    else -> false\n                }\n            }\n        if (currentIndex > 0) {\n            val prevVideos = availableVideoList.subList(0, currentIndex)\n            val prevVideo =\n                prevVideos.lastOrNull { it is VideoListItemData } as? VideoListItemData\n            if (prevVideo != null) {\n                logger.info { \"Play previous video: $prevVideo\" }\n                partTitle = prevVideo.title\n                loadPlayUrl(\n                    avid = prevVideo.aid,\n                    cid = prevVideo.cid!!,\n                    epid = prevVideo.epid,\n                    seasonId = prevVideo.seasonId,\n                    continuePlayNext = true\n                )\n                return true\n            }\n        }\n        return false\n    }\n\n    /**\n     * 加载直播流（带画质信息）\n     * @param roomId 直播间ID\n     * @param qn 请求的画质编号，默认30000（最高值，服务端会自动降级）\n     */\n    fun loadLiveStreamWithQuality(roomId: Int, qn: Int = 30000) {\n        // 取消之前的重连任务\n        liveRetryJob?.cancel()\n        liveRetryJob = null\n        // 取消之前的URL刷新任务\n        liveUrlRefreshJob?.cancel()\n        liveUrlRefreshJob = null\n        // 重置刷新失败计数\n        consecutiveRefreshFailures = 0\n        // 标记播放器为直播模式\n        videoPlayer?.isLive = true\n\n        viewModelScope.launch(Dispatchers.IO) {\n            logger.fInfo { \"Load live stream with quality: roomId=$roomId, qn=$qn\" }\n            withContext(Dispatchers.Main) { loadState = RequestState.Doing }\n\n            // 仅在首次加载时初始化弹幕播放器，画质切换时不重复创建\n            if (danmakuView == null) {\n                ensureDanmakuView()\n            }\n\n            val playInfo = LiveStreamUrlFetcher.fetchLiveStreamUrl(roomId, qn, currentLiveCodec)\n            if (playInfo == null) {\n                withContext(Dispatchers.Main) {\n                    loadState = RequestState.Failed\n                    errorMessage = \"获取直播流失败\"\n                }\n                return@launch\n            }\n\n            withContext(Dispatchers.Main) {\n                liveStreamUrl = playInfo.streamUrl\n                liveStreamExpiresAt = playInfo.expiresAt\n                currentLiveQn = playInfo.currentQn\n                liveQnDescMap = playInfo.qnDescMap\n\n                // 更新可用画质列表（按 qn 降序，即最高画质在前）\n                val qualities = playInfo.acceptQn\n                    .sortedDescending()\n                    .map { qualityQn ->\n                        qualityQn to (playInfo.qnDescMap[qualityQn] ?: \"未知画质 $qualityQn\")\n                    }\n                availableLiveQualities.clear()\n                availableLiveQualities.addAll(qualities)\n\n                currentLiveQualityDescription = playInfo.qnDescMap[playInfo.currentQn] ?: \"未知画质\"\n                logger.fInfo { \"Live quality: current=${playInfo.currentQn} ($currentLiveQualityDescription), available=$qualities\" }\n                logger.fDebug { \"Live stream URL expires at: ${playInfo.expiresAt}\" }\n            }\n\n            runCatching {\n                withContext(Dispatchers.Main) {\n                    videoPlayer?.playUrl(videoUrl = playInfo.streamUrl)\n                    videoPlayer?.prepare()\n                    videoPlayer?.start()\n                    loadState = RequestState.Success\n                }\n                logger.fInfo { \"Live stream loaded successfully with quality ${playInfo.currentQn}\" }\n                // 播放成功后自动启动直播弹幕（仅首次加载，画质切换时不重启弹幕）\n                if (liveWebSocket == null) {\n                    startLiveDanmaku(roomId)\n                }\n                // 调度URL刷新\n                scheduleLiveUrlRefresh()\n            }.onFailure { e ->\n                logger.fError { \"Failed to load live stream: ${e.message}\" }\n                withContext(Dispatchers.Main) {\n                    loadState = RequestState.Failed\n                    errorMessage = \"加载直播流失败: ${e.message}\"\n                }\n            }\n        }\n    }\n\n    /**\n     * 切换直播画质\n     * @param qn 目标画质编号\n     */\n    fun changeLiveQuality(qn: Int) {\n        logger.fInfo { \"Change live quality to: $qn\" }\n        loadLiveStreamWithQuality(liveRoomId, qn)\n    }\n\n    /**\n     * 切换直播编码格式\n     * @param codec 目标编码格式\n     */\n    fun changeLiveCodec(codec: LiveCodec) {\n        logger.fInfo { \"Change live codec to: $codec\" }\n        currentLiveCodec = codec\n        Prefs.defaultLiveCodec = codec\n        loadLiveStreamWithQuality(liveRoomId, currentLiveQn)\n    }\n\n    /**\n     * 直播流错误时自动重连\n     * 延迟 2 秒后重新获取直播流 URL 并播放。\n     * 使用 liveRetryJob 做防抖：新的重连请求会取消上一次未执行的延迟重试。\n     * 当直播间已关闭（liveStatus != 1）时，fetchLiveStreamUrl 返回 null，自动停止重试。\n     */\n    fun retryLiveStream() {\n        if (!isLive) return\n        logger.fInfo { \"Scheduling live stream retry in 2s for room $liveRoomId\" }\n\n        // 防抖：取消上一次待执行的重试\n        liveRetryJob?.cancel()\n        // 取消URL刷新任务\n        liveUrlRefreshJob?.cancel()\n        liveUrlRefreshJob = null\n\n        liveRetryJob = viewModelScope.launch(Dispatchers.IO) {\n            delay(2000)\n            // 仅在播放器未在播放时重试\n            val playing = withContext(Dispatchers.Main) { videoPlayer?.isPlaying == true }\n            if (playing) {\n                logger.fInfo { \"Player is already playing, skip retry\" }\n                return@launch\n            }\n            logger.fInfo { \"Retrying live stream for room $liveRoomId, qn=$currentLiveQn\" }\n            withContext(Dispatchers.Main) {\n                loadState = RequestState.Doing\n                // 重连时先清除错误状态，让 UI 不再显示错误\n                errorMessage = \"\"\n            }\n            val playInfo = LiveStreamUrlFetcher.fetchLiveStreamUrl(liveRoomId, currentLiveQn, currentLiveCodec)\n            if (playInfo == null) {\n                // fetchLiveStreamUrl 内部已判断 liveStatus != 1 并 Toast \"主播未开播\"\n                // 此时不再继续重试\n                logger.fInfo { \"Live stream fetch returned null, live may have ended\" }\n                withContext(Dispatchers.Main) {\n                    loadState = RequestState.Failed\n                    errorMessage = \"直播已结束或获取直播流失败\"\n                }\n                return@launch\n            }\n            // 成功获取新 URL，重新播放\n            withContext(Dispatchers.Main) {\n                liveStreamUrl = playInfo.streamUrl\n                liveStreamExpiresAt = playInfo.expiresAt\n                currentLiveQn = playInfo.currentQn\n                videoPlayer?.playUrl(videoUrl = playInfo.streamUrl)\n                videoPlayer?.prepare()\n                videoPlayer?.start()\n                loadState = RequestState.Success\n            }\n            logger.fInfo { \"Live stream retry successful, new URL loaded\" }\n            // 重置刷新失败计数并重新调度刷新\n            consecutiveRefreshFailures = 0\n            scheduleLiveUrlRefresh()\n        }\n    }\n\n    /**\n     * 调度直播流URL的主动刷新\n     * 在URL过期前REFRESH_BEFORE_EXPIRY_MS毫秒自动刷新\n     */\n    private fun scheduleLiveUrlRefresh() {\n        // 取消之前的刷新任务\n        liveUrlRefreshJob?.cancel()\n\n        if (!isLive || liveStreamExpiresAt <= 0) {\n            logger.fDebug { \"No need to schedule refresh: isLive=$isLive, expiresAt=$liveStreamExpiresAt\" }\n            return\n        }\n\n        val now = System.currentTimeMillis()\n        val timeUntilExpiry = liveStreamExpiresAt - now\n        val refreshDelay = (timeUntilExpiry - REFRESH_BEFORE_EXPIRY_MS)\n            .coerceAtLeast(MIN_REFRESH_INTERVAL_MS)\n\n        logger.fInfo { \"Scheduling live URL refresh in ${refreshDelay}ms (expires at $liveStreamExpiresAt)\" }\n\n        liveUrlRefreshJob = viewModelScope.launch(Dispatchers.IO) {\n            delay(refreshDelay)\n            refreshLiveStreamUrl()\n        }\n    }\n\n    /**\n     * 刷新直播流URL（无缝切换）\n     */\n    private suspend fun refreshLiveStreamUrl() {\n        if (!isLive) return\n\n        logger.fInfo { \"Refreshing live stream URL for room $liveRoomId\" }\n\n        try {\n            val playInfo = LiveStreamUrlFetcher.fetchLiveStreamUrl(\n                liveRoomId,\n                currentLiveQn,\n                currentLiveCodec\n            )\n\n            if (playInfo == null) {\n                // 刷新失败，可能是直播已结束\n                consecutiveRefreshFailures++\n                logger.fWarn { \"Failed to refresh live URL (attempt $consecutiveRefreshFailures), live may have ended\" }\n\n                if (consecutiveRefreshFailures >= MAX_REFRESH_FAILURES) {\n                    // 多次失败，可能直播已结束，停止刷新\n                    logger.fWarn { \"Max refresh failures reached, stopping refresh\" }\n                    withContext(Dispatchers.Main) {\n                        loadState = RequestState.Failed\n                        errorMessage = \"直播可能已结束\"\n                    }\n                    return\n                }\n\n                // 如果直播未结束但刷新失败，稍后重试\n                delay(REFRESH_RETRY_INTERVAL_MS)\n                scheduleLiveUrlRefresh()\n                return\n            }\n\n            // 重置失败计数\n            consecutiveRefreshFailures = 0\n\n            // 更新URL和过期时间\n            withContext(Dispatchers.Main) {\n                liveStreamUrl = playInfo.streamUrl\n                liveStreamExpiresAt = playInfo.expiresAt\n                currentLiveQn = playInfo.currentQn\n            }\n\n            // 无缝切换：更新播放器URL\n            withContext(Dispatchers.Main) {\n                videoPlayer?.playUrl(videoUrl = playInfo.streamUrl)\n            }\n\n            logger.fInfo { \"Live URL refreshed successfully, new expiresAt=$liveStreamExpiresAt\" }\n\n            // 调度下一次刷新\n            scheduleLiveUrlRefresh()\n        } catch (e: Exception) {\n            logger.fError { \"Error refreshing live URL: ${e.message}\" }\n            consecutiveRefreshFailures++\n            if (consecutiveRefreshFailures < MAX_REFRESH_FAILURES) {\n                delay(REFRESH_RETRY_INTERVAL_MS)\n                scheduleLiveUrlRefresh()\n            }\n        }\n    }\n\n    /**\n     * 加载直播流\n     */\n    fun loadLiveStream(streamUrl: String) {\n        viewModelScope.launch(Dispatchers.IO) {\n            logger.fInfo { \"Load live stream: $streamUrl\" }\n            withContext(Dispatchers.Main) { loadState = RequestState.Doing }\n\n            // 初始化弹幕播放器\n            ensureDanmakuView()\n            logger.fInfo { \"Danmaku view ready for live stream\" }\n\n            runCatching {\n                withContext(Dispatchers.Main) {\n                    videoPlayer?.playUrl(videoUrl = streamUrl)\n                    videoPlayer?.prepare()\n                    videoPlayer?.start()\n                    loadState = RequestState.Success\n                }\n                logger.fInfo { \"Live stream loaded successfully\" }\n            }.onFailure { e ->\n                logger.fError { \"Failed to load live stream: ${e.message}\" }\n                withContext(Dispatchers.Main) {\n                    loadState = RequestState.Failed\n                    errorMessage = \"加载直播流失败: ${e.message}\"\n                }\n            }\n        }\n    }\n\n    /**\n     * 启动直播弹幕\n     */\n    fun startLiveDanmaku(roomId: Int) {\n        if (roomId <= 0) {\n            logger.fWarn { \"Invalid room id: $roomId\" }\n            return\n        }\n\n        logger.fInfo { \"Starting live danmaku for room $roomId\" }\n        stopLiveDanmaku()\n\n        // 连接 WebSocket\n        liveWebSocket = viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                logger.fInfo { \"Getting live danmaku info for room $roomId\" }\n                val danmuInfo = BiliLiveHttpApi.getLiveDanmuInfo(roomId, sessData = Prefs.sessData)\n                logger.fInfo { \"Danmaku info response: code=${danmuInfo.code}, message=${danmuInfo.message}\" }\n\n                if (danmuInfo.data == null) {\n                    logger.fError { \"Failed to get danmaku info: data is null\" }\n                    return@launch\n                }\n\n                logger.fInfo { \"Getting live room play info for room $roomId\" }\n                val playInfo = BiliLiveHttpApi.getLiveRoomPlayInfo(roomId)\n                logger.fInfo { \"Play info response: code=${playInfo.code}, message=${playInfo.message}\" }\n\n                val realRoomId = playInfo.data?.roomId\n                if (realRoomId == null) {\n                    logger.fError { \"Failed to get real room id: data.roomId is null\" }\n                    return@launch\n                }\n\n                logger.fInfo { \"Real room id: $realRoomId, starting WebSocket connection\" }\n\n                // 创建 Channel 和单消费者协程，避免每条弹幕创建一个协程\n                val channel = Channel<DanmakuEvent>(capacity = Channel.BUFFERED)\n                liveDanmakuChannel = channel\n                liveDanmakuConsumer = viewModelScope.launch(Dispatchers.IO) {\n                    for (event in channel) {\n                        addLiveDanmaku(event)\n                    }\n                }\n\n                logger.fInfo { \"Connecting to live danmaku WebSocket for room $realRoomId\" }\n                // 使用预取的 token 和 hostList，避免 connectLiveEvent 内部重复调用 API\n                liveWebSocketInner = LiveDataWebSocket.connectLiveEvent(\n                    realRoomId = realRoomId,\n                    token = danmuInfo.data!!.token,\n                    hostList = danmuInfo.data!!.hostList,\n                    uid = Prefs.uid,\n                ) { event ->\n                    when (event) {\n                        is DanmakuEvent -> channel.trySend(event)\n                        is PopularityChangeEvent -> {\n                            val now = System.currentTimeMillis()\n                            if (now - lastPopularityUpdateTime >= 10_000) {\n                                livePopularityText = event.popularityText\n                                lastPopularityUpdateTime = now\n                            }\n                        }\n                        is OnlineRankCountEvent -> {\n                            val now = System.currentTimeMillis()\n                            if (now - lastOnlineCountUpdateTime >= 10_000) {\n                                liveOnlineCount = \"${event.count} 高能观众\"\n                                lastOnlineCountUpdateTime = now\n                            }\n                        }\n                    }\n                }\n            }.onFailure { e ->\n                logger.fError { \"Live danmaku connection failed: ${e.message}\\n${e.stackTraceToString()}\" }\n            }\n        }\n\n        // 启动批量发送定时任务\n        startLiveDanmakuFlushJob()\n\n        logger.fInfo { \"Live danmaku started\" }\n    }\n\n    /**\n     * 停止直播弹幕\n     */\n    fun stopLiveDanmaku() {\n        logger.fInfo { \"Stopping live danmaku\" }\n\n        // 停止批量发送定时任务\n        stopLiveDanmakuFlushJob()\n\n        liveWebSocket?.cancel()\n        liveWebSocket = null\n        liveWebSocketInner?.cancel()\n        liveWebSocketInner = null\n        liveDanmakuChannel?.close()\n        liveDanmakuChannel = null\n        liveDanmakuConsumer?.cancel()\n        liveDanmakuConsumer = null\n\n        // 清空缓冲区\n        synchronized(liveDanmakuBuffer) {\n            liveDanmakuBuffer.clear()\n        }\n\n        logger.fInfo { \"Live danmaku stopped\" }\n    }\n\n    /**\n     * 添加直播弹幕到缓冲区\n     */\n    private fun addLiveDanmaku(event: DanmakuEvent) {\n        // 添加用户等级过滤逻辑\n        if (event.userLevel < currentLiveDanmakuFilterLevel) {\n            logger.fInfo { \"Filtered live danmaku: userLevel=${event.userLevel} < $currentLiveDanmakuFilterLevel\" }\n            return\n        }\n\n        val danmakuItem = Danmaku(\n            dmid = System.currentTimeMillis(),\n            positionMs = 0,\n            text = event.content,\n            mode = event.mode,\n            textSize = event.fontSize,\n            color = 0xFF000000.toInt() or (event.color and 0xFFFFFF)\n        )\n\n        // 添加到缓冲区\n        synchronized(liveDanmakuBuffer) {\n            liveDanmakuBuffer.add(danmakuItem)\n        }\n    }\n\n    /**\n     * 启动直播弹幕批量发送定时任务\n     */\n    private fun startLiveDanmakuFlushJob() {\n        liveDanmakuFlushJob?.cancel()\n        liveDanmakuFlushJob = viewModelScope.launch(Dispatchers.IO) {\n            while (isActive) {\n                delay(1000) // 每秒执行一次\n                flushLiveDanmakuBuffer()\n            }\n        }\n    }\n\n    /**\n     * 批量发送缓冲区中的弹幕\n     */\n    private suspend fun flushLiveDanmakuBuffer() {\n        val itemsToSend = synchronized(liveDanmakuBuffer) {\n            if (liveDanmakuBuffer.isEmpty()) return@synchronized emptyList()\n            val items = liveDanmakuBuffer.toList()\n            liveDanmakuBuffer.clear()\n            items\n        }\n\n        if (itemsToSend.isEmpty()) return\n\n        val view = danmakuView ?: return\n\n        // positionMs 必须与 positionProvider 使用同一时钟（SystemClock.elapsedRealtime），\n        // 否则 DanmakuEngine 的 skipOld/dropIfLagging 会丢弃\"过时\"弹幕\n        val nowMs = android.os.SystemClock.elapsedRealtime().toInt()\n        val updatedItems = itemsToSend.map {\n            it.copy(positionMs = nowMs)\n        }\n\n        withContext(Dispatchers.Main) {\n            view.appendDanmakus(updatedItems, maxItems = 5000, alreadySorted = true)\n        }\n    }\n\n    /**\n     * 停止直播弹幕批量发送定时任务\n     */\n    private fun stopLiveDanmakuFlushJob() {\n        liveDanmakuFlushJob?.cancel()\n        liveDanmakuFlushJob = null\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/DynamicViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.home\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport dev.aaa1115910.biliapi.entity.user.DynamicItem\nimport dev.aaa1115910.biliapi.entity.user.DynamicVideo\nimport dev.aaa1115910.biliapi.http.entity.AuthFailureException\nimport dev.aaa1115910.biliapi.repositories.UserRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.addAllWithMainContext\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.fWarn\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\nimport dev.aaa1115910.bv.repository.UserRepository as BvUserRepository\n\n@KoinViewModel\nclass DynamicViewModel(\n    private val bvUserRepository: BvUserRepository,\n    private val userRepository: UserRepository\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger {}\n    }\n\n    val dynamicVideoList = mutableStateListOf<DynamicVideo>()\n    val dynamicAllList = mutableStateListOf<DynamicItem>()\n\n    private var currentVideoPage = 0\n    var loadingVideo = false\n    var videoHasMore = true\n    private var videoHistoryOffset: String? = null\n    private var videoUpdateBaseline: String? = null\n\n    private var currentAllPage = 0\n    var loadingAll by mutableStateOf(false)\n    var allHasMore by mutableStateOf(true)\n    private var allHistoryOffset: String? = null\n    private var allUpdateBaseline: String? = null\n\n    val isLogin get() = bvUserRepository.isLogin\n\n    init {\n        println(\"=====init DynamicViewModel\")\n    }\n\n    suspend fun loadMoreVideo() {\n        if (!loadingVideo) loadVideoData()\n    }\n\n    suspend fun loadMoreAll() {\n        if (!loadingAll) loadAllData()\n    }\n\n    private suspend fun loadVideoData() {\n        if (!videoHasMore || !bvUserRepository.isLogin) return\n        loadingVideo = true\n        logger.fInfo { \"Load more dynamic videos [apiType=${Prefs.apiType}, offset=$videoHistoryOffset, page=${currentVideoPage + 1}]\" }\n        runCatching {\n            val dynamicVideoData = userRepository.getDynamicVideos(\n                page = ++currentVideoPage,\n                offset = videoHistoryOffset ?: \"\",\n                updateBaseline = videoUpdateBaseline ?: \"\",\n                preferApiType = Prefs.apiType\n            )\n            dynamicVideoList.addAllWithMainContext(dynamicVideoData.videos)\n            videoHistoryOffset = dynamicVideoData.historyOffset\n            videoUpdateBaseline = dynamicVideoData.updateBaseline\n            videoHasMore = dynamicVideoData.hasMore\n\n            logger.fInfo { \"Load dynamic video list page: ${currentVideoPage},size: ${dynamicVideoData.videos.size}\" }\n            val avList = dynamicVideoData.videos.map {\n                it.aid\n            }\n            logger.fInfo { \"Load dynamic video size: ${avList.size}\" }\n            logger.info { \"Load dynamic video list ${avList}}\" }\n        }.onFailure {\n            logger.fWarn { \"Load dynamic video list failed: ${it.stackTraceToString()}\" }\n            when (it) {\n                is AuthFailureException -> {\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.exception_auth_failure)\n                            .toast(BVApp.context)\n                    }\n                    logger.fInfo { \"User auth failure\" }\n                    if (!BuildConfig.DEBUG) bvUserRepository.logout()\n                }\n\n                else -> {\n                    withContext(Dispatchers.Main) {\n                        \"加载动态失败: ${it.localizedMessage}\".toast(BVApp.context)\n                    }\n                }\n            }\n        }\n        withContext(Dispatchers.Main) {\n            loadingVideo = false\n        }\n    }\n\n    private suspend fun loadAllData() {\n        if (!allHasMore || !bvUserRepository.isLogin) return\n        loadingAll = true\n        logger.fInfo { \"Load more dynamic all [apiType=${Prefs.apiType}, offset=$allHistoryOffset, page=${currentVideoPage + 1}]\" }\n        runCatching {\n            val dynamicData = userRepository.getDynamics(\n                page = ++currentVideoPage,\n                offset = allHistoryOffset ?: \"\",\n                updateBaseline = allUpdateBaseline ?: \"\",\n                preferApiType = Prefs.apiType\n            )\n            dynamicAllList.addAll(dynamicData.dynamics)\n            allHistoryOffset = dynamicData.historyOffset\n            allUpdateBaseline = dynamicData.updateBaseline\n            allHasMore = dynamicData.hasMore\n\n            logger.fInfo { \"Load dynamic all list page: ${currentVideoPage},size: ${dynamicData.dynamics.size}\" }\n        }.onFailure {\n            logger.fWarn { \"Load dynamic all list failed: ${it.stackTraceToString()}\" }\n            when (it) {\n                is AuthFailureException -> {\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.exception_auth_failure)\n                            .toast(BVApp.context)\n                    }\n                    logger.fInfo { \"User auth failure\" }\n                }\n\n                else -> {\n                    withContext(Dispatchers.Main) {\n                        \"加载动态失败: ${it.localizedMessage}\".toast(BVApp.context)\n                    }\n                }\n            }\n        }\n        withContext(Dispatchers.Main) {\n            loadingAll = false\n        }\n    }\n\n    fun clearVideoData() {\n        dynamicVideoList.clear()\n        currentVideoPage = 0\n        loadingVideo = false\n        videoHasMore = true\n        videoHistoryOffset = null\n    }\n\n    fun clearAllData() {\n        dynamicAllList.clear()\n        currentAllPage = 0\n        loadingAll = false\n        allHasMore = true\n        allHistoryOffset = null\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/PopularViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.home\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport dev.aaa1115910.biliapi.entity.rank.PopularVideoPage\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\nimport dev.aaa1115910.biliapi.repositories.RecommendVideoRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.addAllWithMainContext\nimport dev.aaa1115910.bv.util.fError\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass PopularViewModel(\n    private val recommendVideoRepository: RecommendVideoRepository\n) : ViewModel() {\n    private val logger = KotlinLogging.logger {}\n    val popularVideoList = mutableStateListOf<UgcItem>()\n\n    private var nextPage = PopularVideoPage()\n    var refreshing by mutableStateOf(false)\n    var loading by mutableStateOf(false)\n\n    init {\n        println(\"=====init PopularViewModel\")\n    }\n\n    suspend fun loadMore(\n        beforeAppendData: () -> Unit = {}\n    ) {\n        if (!loading) loadData(\n            beforeAppendData = beforeAppendData\n        )\n    }\n\n    private suspend fun loadData(\n        beforeAppendData: () -> Unit\n    ) {\n        loading = true\n        logger.fInfo { \"Load more popular videos\" }\n        runCatching {\n            val popularVideoData = recommendVideoRepository.getPopularVideos(\n                page = nextPage,\n                preferApiType = Prefs.apiType\n            )\n            beforeAppendData()\n            nextPage = popularVideoData.nextPage\n            popularVideoList.addAllWithMainContext(popularVideoData.list)\n        }.onFailure {\n            logger.fError { \"Load popular video list failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"加载热门视频失败: ${it.localizedMessage}\".toast(BVApp.context)\n            }\n        }\n        loading = false\n    }\n\n    fun clearData() {\n        popularVideoList.clear()\n        resetPage()\n        loading = false\n    }\n\n    fun resetPage() {\n        nextPage = PopularVideoPage()\n        refreshing = true\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/RecommendViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.home\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport dev.aaa1115910.biliapi.entity.home.RecommendPage\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\nimport dev.aaa1115910.biliapi.repositories.RecommendVideoRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.addAllWithMainContext\nimport dev.aaa1115910.bv.util.fError\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass RecommendViewModel(\n    private val recommendVideoRepository: RecommendVideoRepository\n) : ViewModel() {\n    private val logger = KotlinLogging.logger {}\n    val recommendVideoList = mutableStateListOf<UgcItem>()\n\n    private var nextPage = RecommendPage()\n    var refreshing by mutableStateOf(true)\n    var loading by mutableStateOf(false)\n\n    suspend fun loadMore(\n        beforeAppendData: () -> Unit = {}\n    ) {\n        var loadCount = 0\n        val maxLoadMoreCount = 3\n        if (!loading) {\n            if (recommendVideoList.size == 0) {\n                // first load data\n                while (recommendVideoList.size < 24 && loadCount < maxLoadMoreCount) {\n                    val emptyFun: () -> Unit = {}\n                    loadData(beforeAppendData = if (loadCount == 0) beforeAppendData else emptyFun)\n                    if (loadCount != 0) logger.fInfo { \"Load more recommend videos because items too less\" }\n                    loadCount++\n                }\n            } else {\n                val emptyFun: () -> Unit = {}\n                loadData(beforeAppendData = if (loadCount == 0) beforeAppendData else emptyFun)\n            }\n        }\n    }\n\n    private suspend fun loadData(\n        beforeAppendData: () -> Unit\n    ) {\n        loading = true\n        logger.fInfo { \"Load more recommend videos\" }\n        runCatching {\n            val recommendData = recommendVideoRepository.getRecommendVideos(\n                page = nextPage,\n                preferApiType = Prefs.apiType\n            )\n            beforeAppendData()\n            nextPage = recommendData.nextPage\n            recommendVideoList.addAllWithMainContext(recommendData.items)\n        }.onFailure {\n            logger.fError { \"Load recommend video list failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"加载推荐视频失败: ${it.localizedMessage}\".toast(BVApp.context)\n            }\n        }\n        loading = false\n    }\n\n    fun clearData() {\n        recommendVideoList.clear()\n        resetPage()\n        loading = false\n    }\n\n    fun resetPage() {\n        nextPage = RecommendPage()\n        refreshing = true\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/index/PgcIndexViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.index\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport dev.aaa1115910.biliapi.entity.pgc.PgcItem\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.entity.pgc.index.PGC_INDEX_ORDER_FIELD\nimport dev.aaa1115910.biliapi.entity.pgc.index.PgcIndexData\nimport dev.aaa1115910.biliapi.entity.pgc.index.PgcIndexOption\nimport dev.aaa1115910.biliapi.entity.pgc.index.PgcIndexSection\nimport dev.aaa1115910.biliapi.repositories.PgcRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.util.addAllWithMainContext\nimport dev.aaa1115910.bv.util.fError\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass PgcIndexViewModel(\n    private val pgcRepository: PgcRepository,\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    val indexResultItems = mutableStateListOf<PgcItem>()\n\n    private var updating = false\n    private var nextPage = PgcIndexData.PgcIndexPage()\n    val noMore get() = nextPage.hasNext.not()\n\n    var pgcType by mutableStateOf(PgcType.Anime)\n\n    var filterSections by mutableStateOf<List<PgcIndexSection>>(emptyList())\n    val selectedFilters = mutableStateMapOf<String, PgcIndexOption>()\n\n    val isFilterReady get() = filterSections.isNotEmpty()\n\n    val activeFilterTags: List<String>\n        get() = filterSections.mapNotNull { section ->\n            val selectedOption = selectedFilters[section.field] ?: return@mapNotNull null\n            val defaultOption = section.options.firstOrNull() ?: return@mapNotNull null\n            selectedOption\n                .takeIf { it.keyword != defaultOption.keyword || it.sort != defaultOption.sort }\n                ?.name\n        }\n\n    val filterSignature: String\n        get() = filterSections.joinToString(\"&\") { section ->\n            val selectedOption = selectedFilters[section.field]\n            \"${section.field}=${selectedOption?.keyword.orEmpty()}:${selectedOption?.sort.orEmpty()}\"\n        }\n\n    suspend fun changePgcType(pgcType: PgcType) {\n        this.pgcType = pgcType\n        clearData()\n        filterSections = emptyList()\n        selectedFilters.clear()\n\n        runCatching {\n            pgcRepository.getPgcIndexCondition(pgcType)\n        }.onSuccess { conditionData ->\n            val sections = conditionData.buildSections()\n            selectedFilters.putAll(buildDefaultFilters(sections))\n            filterSections = sections\n        }.onFailure {\n            logger.fError { \"Load $pgcType index conditions failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"加载 $pgcType 筛选条件失败: ${it.localizedMessage}\".toast(BVApp.context)\n            }\n        }\n    }\n\n    fun updateFilter(option: PgcIndexOption) {\n        val currentOption = selectedFilters[option.field]\n        if (currentOption == option) return\n        selectedFilters[option.field] = option\n    }\n\n    fun resetFilters() {\n        selectedFilters.clear()\n        selectedFilters.putAll(buildDefaultFilters(filterSections))\n    }\n\n    suspend fun loadMore() {\n        if (!isFilterReady) return\n        if (!updating) loadData()\n    }\n\n    private suspend fun loadData() {\n        updating = true\n        if (!nextPage.hasNext) {\n            updating = false\n            return\n        }\n        runCatching {\n            val selectedOrder = selectedFilters[PGC_INDEX_ORDER_FIELD]\n                ?: error(\"PGC index order is not initialized\")\n            val result = pgcRepository.getPgcIndex(\n                pgcType = pgcType,\n                order = selectedOrder.keyword,\n                sort = selectedOrder.sort ?: \"0\",\n                filters = selectedFilters.entries\n                    .asSequence()\n                    .filter { it.key != PGC_INDEX_ORDER_FIELD }\n                    .associate { it.key to it.value.keyword },\n                page = nextPage\n            )\n            indexResultItems.addAllWithMainContext(result.list)\n            nextPage = result.nextPage\n            logger.info { \"load more $pgcType list success, size: ${result.list.size}\" }\n        }.onFailure {\n            logger.fError { \"Load $pgcType index list failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"加载 $pgcType 索引失败: ${it.localizedMessage}\".toast(BVApp.context)\n            }\n        }\n        updating = false\n    }\n\n    private fun buildDefaultFilters(sections: List<PgcIndexSection>): Map<String, PgcIndexOption> {\n        return sections.mapNotNull { section ->\n            section.options.firstOrNull()?.let { option -> section.field to option }\n        }.toMap()\n    }\n\n    fun clearData() {\n        indexResultItems.clear()\n        nextPage = PgcIndexData.PgcIndexPage()\n        updating = false\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/live/LiveViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.live\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.live.LiveAreaItem\nimport dev.aaa1115910.biliapi.entity.live.LiveRoomItem\nimport dev.aaa1115910.biliapi.repositories.LiveRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.CoroutineStart\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n/**\n * 直播页面的显示模式\n */\nenum class LiveMode {\n    /** 推荐直播 */\n    RECOMMEND,\n    /** 关注的直播 */\n    FOLLOWING,\n    /** 分区直播 */\n    AREA\n}\n\n@KoinViewModel\nclass LiveViewModel(\n    private val liveRepository: LiveRepository\n) : ViewModel() {\n    private val logger = KotlinLogging.logger(\"LiveViewModel\")\n    private var roomLoadJob: Job? = null\n\n    /**\n     * 当前直播模式\n     */\n    var currentMode by mutableStateOf(LiveMode.RECOMMEND)\n        private set\n\n    /**\n     * 主分区列表（父分区组）\n     */\n    val parentAreaGroups = mutableStateListOf<dev.aaa1115910.biliapi.entity.live.LiveAreaGroup>()\n\n    /**\n     * 主分区数据是否已完成加载（无论成功或失败）\n     */\n    var areaGroupsLoadCompleted by mutableStateOf(false)\n        private set\n\n    /**\n     * 当前选中的主分区组\n     */\n    var currentParentGroup by mutableStateOf<dev.aaa1115910.biliapi.entity.live.LiveAreaGroup?>(null)\n\n    /**\n     * 当前主分区下的子分区列表\n     */\n    val subAreaList = mutableStateListOf<LiveAreaItem>()\n\n    /**\n     * 当前选中的子分区\n     */\n    var currentSubArea by mutableStateOf<LiveAreaItem?>(null)\n\n    /**\n     * 当前分区的直播间列表\n     */\n    val roomList = mutableStateListOf<LiveRoomItem>()\n\n    /**\n     * 是否正在加载\n     */\n    var loading by mutableStateOf(false)\n\n    /**\n     * 是否有下一页\n     */\n    var hasMore by mutableStateOf(true)\n\n    /**\n     * 当前页码\n     */\n    private var currentPage = 1\n\n    /**\n     * 上次聚焦的直播间索引（用于从播放器返回时恢复焦点）\n     */\n    var lastFocusedRoomIndex by mutableStateOf(0)\n\n    /** 是否已登录（用于判断是否显示关注tab） */\n    val isLoggedIn: Boolean\n        get() = !liveRepository.sessionData.isNullOrBlank()\n\n    init {\n        loadAreas()\n    }\n\n    /** 当前模式已就绪但列表为空时，触发一次首次加载。 */\n    fun ensureRoomsLoaded() {\n        if (roomList.isEmpty() && !loading) {\n            loadRooms(refresh = true)\n        }\n    }\n\n    /**\n     * 切换到推荐模式\n     */\n    fun switchToRecommend() {\n        if (currentMode == LiveMode.RECOMMEND) return\n        currentMode = LiveMode.RECOMMEND\n        currentParentGroup = null\n        subAreaList.clear()\n        currentSubArea = null\n        loadRooms(refresh = true)\n    }\n\n    /**\n     * 切换到关注模式\n     */\n    fun switchToFollowing() {\n        if (currentMode == LiveMode.FOLLOWING) return\n        currentMode = LiveMode.FOLLOWING\n        currentParentGroup = null\n        subAreaList.clear()\n        currentSubArea = null\n        loadRooms(refresh = true)\n    }\n\n    /**\n     * 加载所有分区\n     */\n    fun loadAreas() {\n        areaGroupsLoadCompleted = false\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                val response = liveRepository.getLiveAreaList()\n                if (response.code == 0) {\n                    withContext(Dispatchers.Main) {\n                        parentAreaGroups.clear()\n                        parentAreaGroups.addAll(response.data)\n                    }\n                    // 缓存分区列表供设置页使用\n                    val cacheString = response.data.joinToString(\",\") { \"${it.id}:${it.name}\" }\n                    Prefs.cachedLiveAreaGroups = cacheString\n                    logger.info { \"Loaded ${response.data.size} parent area groups\" }\n                } else {\n                    withContext(Dispatchers.Main) {\n                        \"加载直播分区失败: ${response.message}\".toast(BVApp.context)\n                    }\n                }\n            }.onFailure { e ->\n                logger.error(e) { \"Failed to load live areas\" }\n                withContext(Dispatchers.Main) {\n                    \"加载直播分区失败: ${e.message}\".toast(BVApp.context)\n                }\n            }\n            withContext(Dispatchers.Main) {\n                areaGroupsLoadCompleted = true\n            }\n        }\n    }\n\n    /**\n     * 切换主分区（进入分区模式）\n     */\n    fun switchParentArea(group: dev.aaa1115910.biliapi.entity.live.LiveAreaGroup) {\n        currentMode = LiveMode.AREA\n        if (currentParentGroup?.id == group.id) return\n        currentParentGroup = group\n        subAreaList.clear()\n        // 添加\"全部\"分区项\n        subAreaList.add(LiveAreaItem(id = \"0\", parentId = group.id.toString(), oldAreaId = \"0\", name = \"全部\", pic = \"\", parentName = group.name, areaType = 0))\n        subAreaList.addAll(group.list)\n        // 默认选中第一个子分区\n        if (subAreaList.isNotEmpty()) {\n            currentSubArea = subAreaList[0]\n            loadRooms(refresh = true)\n        }\n    }\n\n    /**\n     * 切换子分区\n     */\n    fun switchSubArea(area: LiveAreaItem) {\n        if (currentSubArea?.id == area.id) return\n        currentSubArea = area\n        loadRooms(refresh = true)\n    }\n\n    /**\n     * 加载直播间列表\n     * @param refresh 是否刷新（清空现有数据）\n     */\n    fun loadRooms(refresh: Boolean = false) {\n        val request = buildRoomLoadRequest(refresh) ?: return\n        val currentJob = roomLoadJob\n        if (currentJob?.isActive == true) {\n            if (!refresh) return\n            currentJob.cancel()\n        }\n\n        loading = true\n        val loadJob = viewModelScope.launch(start = CoroutineStart.LAZY) {\n            val activeJob = coroutineContext[Job]\n            if (refresh) {\n                currentPage = 1\n                roomList.clear()\n                hasMore = true\n            }\n\n            try {\n                val result = withContext(Dispatchers.IO) {\n                    when (request.mode) {\n                        LiveMode.RECOMMEND -> loadRecommendRooms(request.page)\n                        LiveMode.FOLLOWING -> loadFollowingRooms(request.page)\n                        LiveMode.AREA -> loadAreaRooms(\n                            area = request.area ?: return@withContext null,\n                            page = request.page\n                        )\n                    }\n                }\n\n                if (roomLoadJob === activeJob && result != null) {\n                    applyRoomLoadResult(request, result)\n                }\n            } catch (e: CancellationException) {\n                throw e\n            } catch (e: Throwable) {\n                logger.error(e) { \"Failed to load live rooms\" }\n                if (roomLoadJob === activeJob) {\n                    \"加载直播间列表失败: ${e.message}\".toast(BVApp.context)\n                }\n            } finally {\n                if (roomLoadJob === activeJob) {\n                    loading = false\n                    roomLoadJob = null\n                }\n            }\n        }\n        roomLoadJob = loadJob\n        loadJob.start()\n    }\n\n    private suspend fun loadRecommendRooms(page: Int): RoomLoadResult {\n        val response = liveRepository.getLiveRecommendList(page = page)\n        if (response.code == 0) {\n            return RoomLoadResult(\n                rooms = response.data?.recommendRoomList?.map { it.toLiveRoomItem() } ?: emptyList(),\n                canAdvancePage = true\n            )\n        } else {\n            error(\"加载推荐直播失败: ${response.message}\")\n        }\n    }\n\n    private suspend fun loadFollowingRooms(page: Int): RoomLoadResult {\n        val response = liveRepository.getLiveFollowingList(page = page, pageSize = 10)\n        if (response.code == 0) {\n            val data = response.data\n            return RoomLoadResult(\n                rooms = data?.list?.map { it.toLiveRoomItem() } ?: emptyList(),\n                canAdvancePage = data != null && page < data.totalPage\n            )\n        } else {\n            error(\"加载关注直播失败: ${response.message}\")\n        }\n    }\n\n    private suspend fun loadAreaRooms(area: LiveAreaItem, page: Int): RoomLoadResult {\n        val response = liveRepository.getLiveRoomList(\n            parentAreaId = area.parentId,\n            areaId = area.id,\n            page = page,\n            pageSize = 30\n        )\n        if (response.code == 0) {\n            return RoomLoadResult(\n                rooms = response.data.list,\n                canAdvancePage = true\n            )\n        } else {\n            error(\"加载直播间列表失败: ${response.message}\")\n        }\n    }\n\n    /**\n     * 刷新当前分区的直播间列表\n     */\n    fun refresh() {\n        loadRooms(refresh = true)\n    }\n\n    /**\n     * 加载更多直播间\n     */\n    fun loadMore() {\n        if (hasMore && !loading) {\n            loadRooms(refresh = false)\n        }\n    }\n\n    private fun buildRoomLoadRequest(refresh: Boolean): RoomLoadRequest? {\n        if (!areaGroupsLoadCompleted) return null\n        val page = if (refresh) 1 else currentPage\n        return when (currentMode) {\n            LiveMode.RECOMMEND -> RoomLoadRequest(mode = LiveMode.RECOMMEND, page = page)\n            LiveMode.FOLLOWING -> RoomLoadRequest(mode = LiveMode.FOLLOWING, page = page)\n            LiveMode.AREA -> currentSubArea?.let {\n                RoomLoadRequest(mode = LiveMode.AREA, area = it, page = page)\n            }\n        }\n    }\n\n    private fun applyRoomLoadResult(request: RoomLoadRequest, result: RoomLoadResult) {\n        val existingIds = roomList.map { it.roomId }.toHashSet()\n        val filteredRooms = result.rooms.filter { it.roomId !in existingIds }\n        roomList.addAll(filteredRooms)\n        hasMore = filteredRooms.isNotEmpty() && result.canAdvancePage\n        if (hasMore) {\n            currentPage = request.page + 1\n        }\n\n        when (request.mode) {\n            LiveMode.RECOMMEND -> logger.info { \"Loaded ${result.rooms.size} recommend rooms, page ${request.page}\" }\n            LiveMode.FOLLOWING -> logger.info { \"Loaded ${result.rooms.size} following rooms, page ${request.page}\" }\n            LiveMode.AREA -> logger.info { \"Loaded ${result.rooms.size} rooms for area ${request.area?.name}, page ${request.page}\" }\n        }\n    }\n}\n\nprivate data class RoomLoadRequest(\n    val mode: LiveMode,\n    val area: LiveAreaItem? = null,\n    val page: Int\n)\n\nprivate data class RoomLoadResult(\n    val rooms: List<LiveRoomItem>,\n    val canAdvancePage: Boolean\n)\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/login/AppQrLoginViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.login\n\nimport android.graphics.BitmapFactory\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.ImageBitmapConfig\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.login.QrLoginState\nimport dev.aaa1115910.biliapi.repositories.LoginRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.entity.AuthData\nimport dev.aaa1115910.bv.repository.UserRepository\nimport dev.aaa1115910.bv.util.BlacklistUtil\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fError\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.timeTask\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\nimport qrcode.QRCode\nimport java.io.ByteArrayInputStream\nimport java.io.ByteArrayOutputStream\nimport java.util.Timer\n\n@KoinViewModel\nclass AppQrLoginViewModel(\n    private val userRepository: UserRepository,\n    private val loginRepository: LoginRepository\n) : ViewModel() {\n    var state by mutableStateOf(QrLoginState.Ready)\n    private val logger = KotlinLogging.logger { }\n    var loginUrl by mutableStateOf(\"\")\n    var qrImage by mutableStateOf(ImageBitmap(1, 1, ImageBitmapConfig.Argb8888))\n    private var key = \"\"\n\n    private var timer = Timer()\n\n    fun requestQRCode() {\n        state = QrLoginState.Ready\n        logger.fInfo { \"Request login qr code\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                withContext(Dispatchers.Main) { state = QrLoginState.RequestingQRCode }\n                val qrLoginData = loginRepository.requestAppQrLogin()\n                loginUrl = qrLoginData.url\n                key = qrLoginData.key\n                logger.fInfo { \"Get login request code url\" }\n                logger.info { qrLoginData.url }\n                withContext(Dispatchers.Main) { generateQRImage() }\n                runCatching { timer.cancel() }\n                timer = timeTask(2000, 2000, \"check qr login result\") {\n                    viewModelScope.launch {\n                        checkLoginResult()\n                    }\n                }\n            }.onFailure {\n                withContext(Dispatchers.Main) {\n                    it.message?.toast(BVApp.context)\n                    state = QrLoginState.Error\n                }\n                logger.fError { \"Get login request code url failed: ${it.stackTraceToString()}\" }\n                timer.cancel()\n            }\n        }\n    }\n\n    fun cancelCheckLoginResultTimer() {\n        timer.cancel()\n    }\n\n    private suspend fun checkLoginResult() {\n        logger.fInfo { \"Check for login result\" }\n        runCatching {\n            val qrLoginResult = loginRepository.checkAppQrLoginState(key)\n            withContext(Dispatchers.Main) { state = qrLoginResult.state }\n            when (state) {\n                QrLoginState.WaitingForScan -> {\n                    logger.fInfo { \"Waiting to scan\" }\n                }\n\n                QrLoginState.WaitingForConfirm -> {\n                    logger.fInfo { \"Waiting to confirm\" }\n                }\n\n                QrLoginState.Expired -> {\n                    logger.fInfo { \"QR expired\" }\n                    timer.cancel()\n                }\n\n                QrLoginState.Success -> {\n                    logger.fInfo { \"Login success\" }\n                    Prefs.buvid3 = loginRepository.getbuvid3()\n\n                    val authData = AuthData(\n                        uid = qrLoginResult.cookies!!.dedeUserId,\n                        uidCkMd5 = qrLoginResult.cookies!!.dedeUserIdCkMd5,\n                        sid = qrLoginResult.cookies!!.sid,\n                        biliJct = qrLoginResult.cookies!!.biliJct,\n                        sessData = qrLoginResult.cookies!!.sessData,\n                        tokenExpiredData = qrLoginResult.cookies!!.expiredDate.time,\n                        accessToken = qrLoginResult.accessToken!!,\n                        refreshToken = qrLoginResult.refreshToken!!\n                    )\n\n                    timer.cancel()\n                    BlacklistUtil.checkUid(Prefs.uid)\n                    userRepository.addUser(authData)\n                }\n\n                else -> {\n                    logger.fInfo { \"This state should not be here: $state\" }\n                }\n            }\n        }.onFailure {\n            if (it is CancellationException) {\n                logger.fInfo { \"Timer job cancelled\" }\n                return@onFailure\n            }\n            withContext(Dispatchers.Main) {\n                it.message?.toast(BVApp.context)\n                state = QrLoginState.Error\n            }\n            logger.fError { \"Check qr state failed: ${it.stackTraceToString()}\" }\n        }\n    }\n\n    private fun generateQRImage() {\n        val output = ByteArrayOutputStream()\n        QRCode(loginUrl).render().writeImage(output)\n        val input = ByteArrayInputStream(output.toByteArray())\n        qrImage = BitmapFactory.decodeStream(input).asImageBitmap()\n        logger.fInfo { \"Generated qr image\" }\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/login/SmsLoginViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.login\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport dev.aaa1115910.biliapi.http.util.generateBuvid\nimport dev.aaa1115910.biliapi.repositories.LoginRepository\nimport dev.aaa1115910.biliapi.repositories.SendSmsState\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.entity.AuthData\nimport dev.aaa1115910.bv.repository.UserRepository\nimport dev.aaa1115910.bv.util.BlacklistUtil\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fDebug\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport org.koin.android.annotation.KoinViewModel\nimport java.net.URL\n\n@KoinViewModel\nclass SmsLoginViewModel(\n    private val userRepository: UserRepository,\n    private val loginRepository: LoginRepository\n) : ViewModel() {\n    private val logger = KotlinLogging.logger { }\n    var sendSmsState by mutableStateOf(SendSmsState.Ready)\n\n    private var phone: Long = 0\n    private val loginSessionId = loginRepository.generateLoginSessionId()\n    private var recaptchaToken: String? = null\n    var geetestChallenge: String? = null\n    var geetestValidate: String? = null\n    private var geetestGt: String? = null\n    private val buvid = generateBuvid()\n    private var captchaKey: String? = null\n\n    suspend fun sendSms(\n        phone: Long,\n        onCaptcha: (challenge: String, gt: String) -> Unit\n    ) {\n        this.phone = phone\n        logger.info { \"Send sms to $phone\" }\n        runCatching {\n            val sendSmsResult = loginRepository.requestSms(\n                phone, loginSessionId, buvid, recaptchaToken, geetestChallenge, geetestValidate\n            )\n            when (sendSmsResult.state) {\n                SendSmsState.Ready -> {\n                    logger.info { \"this state should be here: $sendSmsState\" }\n                    withContext(Dispatchers.Main) { sendSmsState = sendSmsResult.state }\n                }\n\n                SendSmsState.Error -> {\n                    logger.warn { \"Send sms failed: ${sendSmsResult.message}\" }\n                    withContext(Dispatchers.Main) {\n                        sendSmsState = sendSmsResult.state\n                        \"发送短信失败：${sendSmsResult.message}\".toast(BVApp.context)\n                    }\n                    clearCaptchaData()\n                }\n\n                SendSmsState.Success -> {\n                    logger.info { \"Send sms success\" }\n                    captchaKey = sendSmsResult.captchaKey\n                    withContext(Dispatchers.Main) {\n                        sendSmsState = sendSmsResult.state\n                        \"验证码已发送\".toast(BVApp.context)\n                    }\n                }\n\n                SendSmsState.RecaptchaRequire -> {\n                    logger.info { \"Require manual recaptcha\" }\n                    logger.info { \"recaptcha url: ${sendSmsResult.recaptchaUrl}\" }\n\n                    URL(sendSmsResult.recaptchaUrl).query.split(\"&\").forEach {\n                        val (key, value) = it.split(\"=\")\n                        when (key) {\n                            \"recaptcha_token\" -> recaptchaToken = value\n                            \"gee_gt\" -> geetestGt = value\n                            \"gee_challenge\" -> geetestChallenge = value\n                        }\n                    }\n\n                    logger.info { \"recaptchaToken: $recaptchaToken\" }\n                    logger.info { \"geetestGt: $geetestGt\" }\n                    logger.info { \"geetestChallenge: $geetestChallenge\" }\n                    onCaptcha(geetestChallenge!!, geetestGt!!)\n                    withContext(Dispatchers.Main) { sendSmsState = sendSmsResult.state }\n                }\n            }\n        }.onFailure {\n            logger.warn { \"Send sms failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"发送短信失败：${it.message}\".toast(BVApp.context)\n                clearCaptchaData()\n            }\n        }\n    }\n\n    suspend fun loginWithSms(code: Int, onSuccess: () -> Unit) {\n        logger.info { \"Login with sms code: $code\" }\n        runCatching {\n            val loginResult = loginRepository.loginWithSms(\n                phone = phone,\n                loginSessionId = loginSessionId,\n                code = code,\n                captchaKey = captchaKey!!\n            )\n            if (loginResult.status == 0) {\n                val authData = AuthData(\n                    uid = loginResult.dedeUserId,\n                    uidCkMd5 = loginResult.dedeUserIdCkMd5,\n                    sid = loginResult.sid,\n                    sessData = loginResult.sessData,\n                    biliJct = loginResult.biliJct,\n                    tokenExpiredData = loginResult.expiredDate.time,\n                    accessToken = loginResult.accessToken,\n                    refreshToken = loginResult.refreshToken\n                )\n                BlacklistUtil.checkUid(Prefs.uid)\n                userRepository.addUser(authData)\n\n                withContext(Dispatchers.Main) {\n                    \"登录成功\".toast(BVApp.context)\n                }\n                logger.info { \"Login with sms success\" }\n                logger.fDebug { \"$loginResult\" }\n                onSuccess()\n            } else {\n                logger.warn { \"Login with sms return a unknown response: [status=${loginResult.status}, message=${loginResult.message}]\" }\n                withContext(Dispatchers.Main) {\n                    \"未知情况：${loginResult.message}\".toast(BVApp.context)\n                }\n            }\n        }.onFailure {\n            logger.warn { \"Login with sms failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"短信登录失败：${it.message}\".toast(BVApp.context)\n            }\n        }\n    }\n\n    fun clearCaptchaData() {\n        logger.info { \"Clear captcha data\" }\n        recaptchaToken = null\n        geetestChallenge = null\n        geetestValidate = null\n        sendSmsState = SendSmsState.Ready\n    }\n}\n\n@Serializable\ndata class GeetestResult(\n    @SerialName(\"geetest_challenge\")\n    val geetestChallenge: String,\n    @SerialName(\"geetest_validate\")\n    val geetestValidate: String,\n    @SerialName(\"geetest_seccode\")\n    val geetestSeccode: String\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcAnimeViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.repositories.PgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass PgcAnimeViewModel(\n    override val pgcRepository: PgcRepository\n) : PgcViewModel(\n    pgcRepository = pgcRepository,\n    pgcType = PgcType.Anime\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcDocumentaryViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.repositories.PgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass PgcDocumentaryViewModel(\n    override val pgcRepository: PgcRepository\n) : PgcViewModel(\n    pgcRepository = pgcRepository,\n    pgcType = PgcType.Documentary\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcGuoChuangViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.repositories.PgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass PgcGuoChuangViewModel(\n    override val pgcRepository: PgcRepository\n) : PgcViewModel(\n    pgcRepository = pgcRepository,\n    pgcType = PgcType.GuoChuang\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcMovieViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.repositories.PgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass PgcMovieViewModel(\n    override val pgcRepository: PgcRepository\n) : PgcViewModel(\n    pgcRepository = pgcRepository,\n    pgcType = PgcType.Movie\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcTvViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.repositories.PgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass PgcTvViewModel(\n    override val pgcRepository: PgcRepository\n) : PgcViewModel(\n    pgcRepository = pgcRepository,\n    pgcType = PgcType.Tv\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcVarietyViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.repositories.PgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass PgcVarietyViewModel(\n    override val pgcRepository: PgcRepository\n) : PgcViewModel(\n    pgcRepository = pgcRepository,\n    pgcType = PgcType.Variety\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/pgc/PgcViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.pgc\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.CarouselData\nimport dev.aaa1115910.biliapi.entity.pgc.PgcFeedData\nimport dev.aaa1115910.biliapi.entity.pgc.PgcItem\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.repositories.PgcRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.util.addWithMainContext\nimport dev.aaa1115910.bv.util.addAllWithMainContext\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.fWarn\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\nabstract class PgcViewModel(\n    open val pgcRepository: PgcRepository,\n    val pgcType: PgcType,\n) : ViewModel() {\n    private val logger = KotlinLogging.logger(\"PgcViewModel[$pgcType]\")\n\n    /**\n     * 轮播图\n     */\n    val carouselItems = mutableStateListOf<CarouselData.CarouselItem>()\n\n    /**\n     * 猜你喜欢\n     */\n    val feedItems = mutableStateListOf<FeedListItem>()\n\n    /**\n     * 推荐数据中会穿插排行榜，为了避免出现某一行仅出现单独几个剧集，因此将不满一行的剧集单独存起来\n     */\n    private val restSubItems = mutableListOf<PgcItem>()\n\n    var updating by mutableStateOf(false)\n    var hasNext by mutableStateOf(true)\n    private var cursor = 0\n\n    fun init() {\n        loadMore()\n        viewModelScope.launch(Dispatchers.IO) {\n            updateCarousel()\n        }\n    }\n\n    /**\n     * 加载更多推荐数据\n     */\n    fun loadMore() {\n        if (hasNext) {\n            viewModelScope.launch(Dispatchers.IO) {\n                updateFeed()\n            }\n        }\n    }\n\n    /**\n     * 重新加载所有数据，点击界面顶部 Tab 时使用\n     */\n    fun reloadAll() {\n        logger.fInfo { \"Reload all $pgcType data\" }\n        clearAll()\n        viewModelScope.launch(Dispatchers.IO) {\n            updateCarousel()\n            updateFeed()\n        }\n    }\n\n    /**\n     * 清理所有数据\n     */\n    fun clearAll() {\n        logger.fInfo { \"Clear all data\" }\n        carouselItems.clear()\n        feedItems.clear()\n        restSubItems.clear()\n        cursor = 0\n        hasNext = true\n    }\n\n    /**\n     * 更新轮播图\n     */\n    private suspend fun updateCarousel() {\n        logger.fInfo { \"Updating $pgcType carousel\" }\n        runCatching {\n            // 由于未知原因，注入的 PgcRepository 可能获取到的对象为 null\n            var maxRetry = 10\n            while (pgcRepository == null && maxRetry > 0) {\n                delay(10)\n                maxRetry--\n            }\n            if (BuildConfig.DEBUG && maxRetry != 10) {\n                logger.fWarn { \"Retry ${10 - maxRetry} times to get pgcRepository\" }\n                withContext(Dispatchers.Main) {\n                    \"Retry ${10 - maxRetry} times to get pgcRepository($pgcType)\".toast(BVApp.context)\n                }\n            }\n\n            val carouselData = pgcRepository.getCarousel(pgcType)\n            logger.fInfo { \"Find $pgcType carousels, size: ${carouselData.items.size}\" }\n            carouselItems.addAllWithMainContext(carouselData.items)\n            logger.debug { \"carouselItems: $carouselItems\" }\n        }.onFailure {\n            logger.fInfo { \"Update $pgcType carousel failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"加载 $pgcType 轮播图失败: ${it.message}\".toast(BVApp.context)\n            }\n        }\n    }\n\n    /**\n     * 获取推荐数据\n     */\n    private suspend fun updateFeed() {\n        if (updating) return\n        withContext(Dispatchers.Main) { updating = true }\n        logger.fInfo { \"Update anime feed\" }\n        runCatching {\n            val pgcFeedData = pgcRepository.getFeed(\n                pgcType = pgcType,\n                cursor = cursor\n            )\n            cursor = pgcFeedData.cursor\n            withContext(Dispatchers.Main) { hasNext = pgcFeedData.hasNext }\n            updateFeedItems(pgcFeedData)\n        }.onFailure {\n            logger.fInfo { \"Update $pgcType feeds failed: ${it.stackTraceToString()}\" }\n        }\n        withContext(Dispatchers.Main) { updating = false }\n    }\n\n    /**\n     * 对 [updateFeed] 获取到得数据进行二次整理并更新到 feedItems\n     */\n    private suspend fun updateFeedItems(data: PgcFeedData) {\n        logger.fInfo { \"update $pgcType feed items: [items: ${data.items.size}, ranks: ${data.ranks.size}]\" }\n        val epList = mutableStateListOf<PgcItem>()\n        epList.addAll(restSubItems)\n        epList.addAll(data.items)\n\n        epList.chunked(5).forEach { chunkedVCardList ->\n            if (chunkedVCardList.size == 5) {\n                feedItems.addWithMainContext(\n                    FeedListItem(\n                        type = FeedListType.Ep,\n                        items = chunkedVCardList\n                    )\n                )\n            } else {\n                restSubItems.clear()\n                restSubItems.addAll(chunkedVCardList)\n            }\n        }\n\n        data.ranks.forEach { rank ->\n            feedItems.addWithMainContext(\n                FeedListItem(\n                    type = FeedListType.Rank,\n                    rank = rank\n                )\n            )\n        }\n    }\n}\n\ndata class FeedListItem(\n    val type: FeedListType,\n    val items: List<PgcItem>? = emptyList(),\n    val rank: PgcFeedData.FeedRank? = null\n)\n\nenum class FeedListType {\n    Ep, Rank\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/search/SearchInputViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.search\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.search.Hotword\nimport dev.aaa1115910.biliapi.repositories.SearchRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.dao.AppDatabase\nimport dev.aaa1115910.bv.entity.db.SearchHistoryDB\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.swapListWithMainContext\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\nimport java.util.Date\n\n@KoinViewModel\nclass SearchInputViewModel(\n    private val searchRepository: SearchRepository,\n    private val db: AppDatabase = BVApp.getAppDatabase()\n) : ViewModel() {\n    private val logger = KotlinLogging.logger { }\n\n    var keyword by mutableStateOf(\"\")\n    val hotwords = mutableStateListOf<Hotword>()\n    val suggests = mutableStateListOf<String>()\n    val searchHistories = mutableStateListOf<SearchHistoryDB>()\n    val matchedSearchHistories = mutableStateListOf<SearchHistoryDB>()\n\n    init {\n        updateHotwords()\n        loadSearchHistories()\n    }\n\n    private fun updateHotwords() {\n        logger.fInfo { \"Update hotwords\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                val hotwordData = searchRepository.getSearchHotwords(\n                    limit = 50,\n                    preferApiType = Prefs.apiType\n                )\n                logger.debug { \"Find hotwords: $hotwordData\" }\n                withContext(Dispatchers.Main) { hotwords.addAll(hotwordData) }\n            }.onFailure {\n                withContext(Dispatchers.Main) {\n                    \"bilibili 热搜加载失败\".toast(BVApp.context)\n                }\n                logger.info { it.stackTraceToString() }\n            }\n        }\n    }\n\n    fun updateSuggests() {\n        logger.fInfo { \"Update search suggests with '$keyword'\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                val keywordSuggest = searchRepository.getSearchSuggest(\n                    keyword = keyword,\n                    preferApiType = Prefs.apiType\n                )\n                logger.debug { \"Find suggests: $keywordSuggest\" }\n                suggests.swapListWithMainContext(keywordSuggest)\n            }.onFailure {\n                withContext(Dispatchers.Main) {\n                    \"bilibili 搜索建议加载失败\".toast(BVApp.context)\n                }\n                logger.info { it.stackTraceToString() }\n            }\n        }\n        updateMatchedSearchHistories()\n    }\n\n    private fun loadSearchHistories() {\n        logger.fInfo { \"Load search histories\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                searchHistories.swapListWithMainContext(db.searchHistoryDao().getHistories(20))\n                logger.fInfo { \"Load search histories finish, size: ${searchHistories.size}\" }\n            }\n        }\n    }\n\n    private fun updateMatchedSearchHistories() {\n        logger.fInfo { \"Update matched search histories with '$keyword'\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                if (keyword.isEmpty()) {\n                    matchedSearchHistories.clear()\n                } else {\n                    val matchedHistories = db.searchHistoryDao().findHistories(keyword, 20)\n                    matchedSearchHistories.swapListWithMainContext(matchedHistories)\n                }\n                logger.fInfo { \"Update matched search histories finish, size: ${matchedSearchHistories.size}\" }\n            }\n        }\n    }\n\n    fun addSearchHistory(keyword: String) {\n        logger.fInfo { \"Add search history: $keyword\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            db.searchHistoryDao().findHistory(keyword)?.let { history ->\n                logger.fInfo { \"Search history $keyword already exist\" }\n                history.searchDate = Date()\n                db.searchHistoryDao().update(history)\n            } ?: let {\n                logger.fInfo { \"Insert new search history $keyword\" }\n                val history = SearchHistoryDB(keyword = keyword)\n                db.searchHistoryDao().insert(history)\n            }\n            loadSearchHistories()\n        }\n    }\n\n    fun deleteSearchHistory(history: SearchHistoryDB) {\n        logger.fInfo { \"Delete search history: ${history.keyword}\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            db.searchHistoryDao().delete(history)\n            loadSearchHistories()\n        }\n    }\n\n    fun deleteAllSearchHistories() {\n        logger.fInfo { \"Delete all search histories\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            db.searchHistoryDao().deleteAll()\n            loadSearchHistories()\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/search/SearchResultViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.search\n\nimport android.content.Context\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.repositories.SearchFilterDuration\nimport dev.aaa1115910.biliapi.repositories.SearchFilterOrderType\nimport dev.aaa1115910.biliapi.repositories.SearchRepository\nimport dev.aaa1115910.biliapi.repositories.SearchType\nimport dev.aaa1115910.biliapi.repositories.SearchTypePage\nimport dev.aaa1115910.biliapi.repositories.SearchTypeResult\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.Partition\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass SearchResultViewModel(\n    private val searchRepository: SearchRepository\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    var keyword by mutableStateOf(\"\")\n    var searchType by mutableStateOf(SearchType.Video)\n\n    var videoSearchResult by mutableStateOf(SearchResult(SearchType.Video))\n    var mediaBangumiSearchResult by mutableStateOf(SearchResult(SearchType.MediaBangumi))\n    var mediaFtSearchResult by mutableStateOf(SearchResult(SearchType.MediaFt))\n    var biliUserSearchResult by mutableStateOf(SearchResult(SearchType.BiliUser))\n    var liveRoomSearchResult by mutableStateOf(SearchResult(SearchType.LiveRoom))\n\n    var selectedOrder by mutableStateOf(SearchFilterOrderType.ComprehensiveSort)\n    var selectedDuration by mutableStateOf(SearchFilterDuration.All)\n    var selectedPartition: Partition? by mutableStateOf(null)\n    var selectedChildPartition: Partition? by mutableStateOf(null)\n\n    private val updating = mutableMapOf<SearchType, Boolean>().apply {\n        SearchType.entries.forEach { put(it, false) }\n    }\n\n    private val hasMore = mutableMapOf<SearchType, Boolean>().apply {\n        SearchType.entries.forEach { put(it, true) }\n    }\n\n    private val pages = mutableMapOf<SearchType, SearchTypePage>().apply {\n        SearchType.entries.forEach { put(it, SearchTypePage()) }\n    }\n\n    private val initeds = mutableMapOf<SearchType, Boolean>().apply {\n        SearchType.entries.forEach { put(it, false) }\n    }\n\n    var enableProxySearchResult = false\n\n    fun update() {\n        resetPages()\n        clearResults()\n        viewModelScope.launch {\n            loadMore(searchType, true)\n        }\n    }\n\n    private fun resetPages() {\n        videoSearchResult.resetPage()\n        mediaBangumiSearchResult.resetPage()\n        mediaFtSearchResult.resetPage()\n        biliUserSearchResult.resetPage()\n        liveRoomSearchResult.resetPage()\n    }\n\n    private fun clearResults() {\n        videoSearchResult.clearResult()\n        mediaBangumiSearchResult.clearResult()\n        mediaFtSearchResult.clearResult()\n        biliUserSearchResult.clearResult()\n        liveRoomSearchResult.clearResult()\n    }\n\n    fun init(searchType: SearchType) {\n        if (initeds[searchType] == false) {\n            loadMore(searchType)\n            initeds[searchType] = true\n        }\n    }\n\n    fun loadMore(\n        searchType: SearchType,\n        ignoreUpdating: Boolean = false\n    ) {\n        if (hasMore[searchType] != true) return\n        if (updating[searchType] == true && !ignoreUpdating) return\n\n        updating[searchType] = true\n        viewModelScope.launch(Dispatchers.IO) {\n            logger.fInfo { \"Load search result: [keyword=$keyword, type=$searchType, page=${pages[searchType]}]\" }\n            runCatching {\n                val searchResultResponse = searchRepository.searchType(\n                    keyword = keyword,\n                    type = searchType,\n                    page = pages[searchType] ?: SearchTypePage(),\n                    tid = selectedChildPartition?.tid ?: selectedPartition?.tid,\n                    order = selectedOrder,\n                    duration = selectedDuration,\n                    preferApiType = Prefs.apiType,\n                    enableProxy = enableProxySearchResult\n                )\n                withContext(Dispatchers.Main) {\n                    when (searchType) {\n                        SearchType.Video -> videoSearchResult =\n                            videoSearchResult.appendSearchResultData(searchResultResponse)\n\n                         SearchType.MediaBangumi -> mediaBangumiSearchResult =\n                             mediaBangumiSearchResult.appendSearchResultData(searchResultResponse)\n\n                         SearchType.MediaFt -> mediaFtSearchResult =\n                             mediaFtSearchResult.appendSearchResultData(searchResultResponse)\n\n                         SearchType.BiliUser -> biliUserSearchResult =\n                             biliUserSearchResult.appendSearchResultData(searchResultResponse)\n\n                        SearchType.LiveRoom -> liveRoomSearchResult =\n                            liveRoomSearchResult.appendSearchResultData(searchResultResponse)\n                    }\n\n                    // 检查返回的数据数量，如果少于请求的分页数量则设置 hasMore 为 false\n                    val returnedCount = when (searchType) {\n                        SearchType.Video -> searchResultResponse.videos.size\n                        SearchType.MediaBangumi -> searchResultResponse.pgcs.size\n                        SearchType.MediaFt -> searchResultResponse.pgcs.size\n                        SearchType.BiliUser -> searchResultResponse.users.size\n                        SearchType.LiveRoom -> searchResultResponse.liveRooms.size\n                    }\n                    val requestedPageSize = 20\n                    if (returnedCount < requestedPageSize) {\n                        hasMore[searchType] = false\n                    }\n                }\n\n                pages[searchType] = searchResultResponse.page\n            }\n            updating[searchType] = false\n        }\n    }\n\n    data class SearchResult(\n        var type: SearchType,\n        var videos: List<SearchTypeResult.Video> = emptyList(),\n        var mediaBangumis: List<SearchTypeResult.Pgc> = emptyList(),\n        var mediaFts: List<SearchTypeResult.Pgc> = emptyList(),\n        var biliUsers: List<SearchTypeResult.User> = emptyList(),\n        var liveRooms: List<SearchTypeResult.LiveRoom> = emptyList(),\n        var page: SearchTypePage = SearchTypePage()\n    ) {\n        val count get() = videos.size + mediaBangumis.size + mediaFts.size + biliUsers.size + liveRooms.size\n\n        fun resetPage() {\n            page = SearchTypePage()\n        }\n\n        fun clearResult() {\n            videos = emptyList()\n            mediaBangumis = emptyList()\n            mediaFts = emptyList()\n            biliUsers = emptyList()\n            liveRooms = emptyList()\n        }\n\n        fun appendSearchResultData(searchTypeResult: SearchTypeResult): SearchResult {\n            return when (type) {\n                SearchType.Video -> {\n                    SearchResult(type).apply {\n                        this.videos = this@SearchResult.videos + searchTypeResult.videos\n                    }\n                }\n\n                SearchType.MediaBangumi -> {\n                    SearchResult(type).apply {\n                        this.mediaBangumis = this@SearchResult.mediaBangumis + searchTypeResult.pgcs\n                    }\n                }\n\n                SearchType.MediaFt -> {\n                    SearchResult(type).apply {\n                        this.mediaFts = this@SearchResult.mediaFts + searchTypeResult.pgcs\n                    }\n                }\n\n                SearchType.BiliUser -> {\n                    SearchResult(type).apply {\n                        this.biliUsers = this@SearchResult.biliUsers + searchTypeResult.users\n                    }\n                }\n\n                SearchType.LiveRoom -> {\n                    SearchResult(type).apply {\n                        this.liveRooms = this@SearchResult.liveRooms + searchTypeResult.liveRooms\n                    }\n                }\n            }\n        }\n    }\n}\n\nenum class SearchResultType(\n    val type: String,\n    private val strRes: Int\n) {\n    Video(type = \"video\", strRes = R.string.search_result_type_name_video),\n    MediaBangumi(type = \"media_bangumi\", R.string.search_result_type_name_media_bangumi),\n    MediaFt(type = \"media_ft\", strRes = R.string.search_result_type_name_media_ft),\n    BiliUser(type = \"bili_user\", strRes = R.string.search_result_type_name_bili_user);\n    // LiveRoom(type = \"live_room\", strRes = R.string.search_result_type_name_live_room);\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcAiViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcAiViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Ai\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcAnimalViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcAnimalViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Animal\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcCarViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcCarViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Car\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcCinephileViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcCinephileViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Cinephile\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcDanceViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcDanceViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Dance\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcDougaViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcDougaViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Douga\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcEmotionViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcEmotionViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Emotion\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcEntViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcEntViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Ent\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcFashionViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcFashionViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Fashion\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcFoodViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcFoodViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Food\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcGameViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcGameViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Game\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcGymViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcGymViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Gym\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcHandmakeViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcHandmakeViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Handmake\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcHealthViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcHealthViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Health\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcHomeViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcHomeViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Home\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcInformationViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcInformationViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Information\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcKichikuViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcKichikuViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Kichiku\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcKnowledgeViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcKnowledgeViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Knowledge\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcLifeExperienceViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcLifeExperienceViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.LifeExperience\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcLifeJoyViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcLifeJoyViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.LifeJoy\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcMusicViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcMusicViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Music\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcMysticismViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcMysticismViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Mysticism\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcOutdoorsViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcOutdoorsViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Outdoors\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcPaintingViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcPaintingViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Painting\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcParentingViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcParentingViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Parenting\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcRuralViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcRuralViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Rural\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcShortplayViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcShortplayViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Shortplay\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcSportsViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcSportsViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Sports\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcTechViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcTechViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Tech\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcTravelViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcTravelViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Travel\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.CarouselData\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.entity.ugc.region.UgcFeedPage\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\nabstract class UgcViewModel(\n    open val ugcRepository: UgcRepository,\n    val ugcType: UgcTypeV2\n) : ViewModel() {\n    private val logger = KotlinLogging.logger(\"UgvViewModel[$ugcType]\")\n\n    /**\n     * 轮播图\n     */\n    val carouselItems = mutableStateListOf<CarouselData.CarouselItem>()\n\n    /**\n     * UGC数据列表\n     */\n    val ugcItems = mutableStateListOf<UgcItem>()\n\n    var nextPage by mutableStateOf(UgcFeedPage())\n    var hasMore by mutableStateOf(true)\n    var updating by mutableStateOf(false)\n    var showCarousel by mutableStateOf(true)\n\n    private var loadJob: Job? = null\n\n    init {\n        // 移除初始化时的自动加载\n    }\n\n    private suspend fun initUgcRegionData() {\n        loadUgcRegionData()\n        // loadMore()\n    }\n\n    suspend fun loadUgcRegionData() {\n        if (!hasMore && updating) return\n        updating = true\n        logger.fInfo { \"load ugc $ugcType region data\" }\n        runCatching {\n            val carouselData = ugcRepository.getCarousel(ugcType)\n            val data = ugcRepository.getRegionFeedRcmd(ugcType, nextPage)\n            carouselItems.clear()\n            ugcItems.clear()\n            carouselItems.addAll(carouselData.items)\n            ugcItems.addAll(data.items)\n            nextPage = data.nextPage\n            showCarousel = carouselItems.isNotEmpty()\n        }.onFailure {\n            logger.fInfo { \"load $ugcType data failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"加载 $ugcType 数据失败: ${it.message}\".toast(BVApp.context)\n            }\n        }\n        hasMore = true\n        updating = false\n    }\n\n    fun reloadAll() {\n        logger.fInfo { \"reload all $ugcType data\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            nextPage = UgcFeedPage()\n            hasMore = true\n            showCarousel = true\n            carouselItems.clear()\n            ugcItems.clear()\n            loadJob?.cancel() // 取消任何正在进行的延迟加载\n            initUgcRegionData()\n        }\n    }\n\n    /**\n     * 延迟加载数据，如果在延迟期间被取消则不加载\n     */\n    fun loadDataWithDelay(delayMs: Long = 300L) {\n        loadJob?.cancel()\n        loadJob = viewModelScope.launch(Dispatchers.IO) {\n            delay(delayMs)\n            if (ugcItems.isEmpty()) {\n                initUgcRegionData()\n            }\n        }\n    }\n\n    /**\n     * 取消延迟加载\n     */\n    fun cancelDelayedLoad() {\n        loadJob?.cancel()\n        loadJob = null\n    }\n\n    suspend fun loadMore() {\n        if (!hasMore && updating) return\n        updating = true\n        runCatching {\n            val data = ugcRepository.getRegionFeedRcmd(ugcType, nextPage)\n            ugcItems.addAll(data.items)\n            nextPage = data.nextPage\n            hasMore = data.items.isNotEmpty()\n        }.onFailure {\n            logger.fInfo { \"load more $ugcType data failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"加载 $ugcType 更多推荐失败: ${it.message}\".toast(BVApp.context)\n            }\n        }\n        updating = false\n    }\n\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/ugc/UgcVlogViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.ugc\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UgcVlogViewModel(\n    override val ugcRepository: UgcRepository\n) : UgcViewModel(\n    ugcRepository = ugcRepository,\n    ugcType = UgcTypeV2.Vlog\n)"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/user/FavoriteViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.user\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.FavoriteFolderMetadata\nimport dev.aaa1115910.biliapi.entity.FavoriteItemType\nimport dev.aaa1115910.biliapi.entity.ugc.toSmartDate\nimport dev.aaa1115910.biliapi.repositories.FavoriteRepository\nimport dev.aaa1115910.bv.BVApp.Companion.context\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.DeviceUtil\nimport dev.aaa1115910.bv.util.addWithMainContext\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.fWarn\nimport dev.aaa1115910.bv.util.swapList\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass FavoriteViewModel(\n    private val favoriteRepository: FavoriteRepository\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    var favoriteFolderMetadataList = mutableStateListOf<FavoriteFolderMetadata>()\n    var favorites = mutableStateListOf<VideoCardData>()\n\n    var currentFavoriteFolderMetadata: FavoriteFolderMetadata? by mutableStateOf(null)\n\n    private var pageSize = 20\n    private var pageNumber = 1\n    private var hasMore = true\n\n    var updatingFolders by mutableStateOf(false)\n    var updatingFolderItems by mutableStateOf(false)\n\n    init {\n        if (!DeviceUtil.isTvDevice()) {\n            updateFoldersInfo()\n        } else {\n            logger.fInfo { \"Skip updating favorite folders on TV device\" }\n        }\n    }\n\n    fun updateFoldersInfo() {\n        if (updatingFolders) return\n        updatingFolders = true\n        logger.fInfo { \"Updating favorite folders\" }\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                val favoriteFolderMetadataList =\n                    favoriteRepository.getAllFavoriteFolderMetadataList(\n                        mid = Prefs.uid,\n                        preferApiType = Prefs.apiType\n                    )\n                withContext(Dispatchers.Main) {\n                    this@FavoriteViewModel.favoriteFolderMetadataList\n                        .swapList(favoriteFolderMetadataList)\n                    currentFavoriteFolderMetadata = favoriteFolderMetadataList.firstOrNull()\n                }\n                logger.fInfo { \"Update favorite folders success: ${favoriteFolderMetadataList.map { it.id }}\" }\n            }.onFailure {\n                logger.fWarn { \"Update favorite folders failed: ${it.stackTraceToString()}\" }\n                //这里返回的数据并不会有用户认证失败的错误返回，没必要做身份验证失败提示\n            }.onSuccess {\n                updateFolderItems()\n            }\n            updatingFolders = false\n        }\n    }\n\n    private var updateJob: Job? = null\n\n    fun updateFolderItems(force: Boolean = false) {\n        if (force) {\n            updateJob?.cancel()\n            resetPageNumber()\n            updatingFolderItems = false\n        }\n        if (updatingFolderItems || !hasMore) return\n        updatingFolderItems = true\n        logger.fInfo { \"Updating favorite folder items with media id: ${currentFavoriteFolderMetadata?.id}\" }\n        updateJob = viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                val favoriteFolderData = favoriteRepository.getFavoriteFolderData(\n                    mediaId = currentFavoriteFolderMetadata!!.id,\n                    pageSize = pageSize,\n                    pageNumber = pageNumber,\n                    preferApiType = Prefs.apiType\n                )\n                favoriteFolderData.medias.forEach { favoriteItem ->\n                    if (favoriteItem.type != FavoriteItemType.Video) return@forEach\n                    favorites.addWithMainContext(\n                        VideoCardData(\n                            avid = favoriteItem.id,\n                            title = favoriteItem.title,\n                            cover = favoriteItem.cover,\n                            play = favoriteItem.cntInfo.play,\n                            danmaku = favoriteItem.cntInfo.danmaku,\n                            upName = favoriteItem.upper.name,\n                            upId = favoriteItem.upper.mid,\n                            upFace = favoriteItem.upper.face,\n                            time = favoriteItem.duration * 1000L,\n                            pubTime = favoriteItem.favTime.toSmartDate() + context.getString(R.string.favorite_at)\n                        )\n                    )\n                }\n                hasMore = favoriteFolderData.hasMore\n                logger.fInfo { \"Update favorite items success\" }\n            }.onFailure {\n                logger.fInfo { \"Update favorite items failed: ${it.stackTraceToString()}\" }\n            }.onSuccess {\n                pageNumber++\n            }\n            updatingFolderItems = false\n        }\n    }\n\n    fun resetPageNumber() {\n        pageNumber = 1\n        hasMore = true\n    }\n\n    fun clearData() {\n        favorites.clear()\n        resetPageNumber()\n        logger.fInfo { \"Favorite data cleared\" }\n    }\n\n    fun removeFavoriteFromList(aid: Long) {\n        favorites.removeAll { it.avid == aid }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/user/FollowViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.user\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.user.FollowedUser\nimport dev.aaa1115910.biliapi.repositories.UserRepository\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.swapListWithMainContext\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.android.annotation.KoinViewModel\nimport java.text.Collator\nimport java.util.Locale\n\n@KoinViewModel\nclass FollowViewModel(\n    private val userRepository: UserRepository\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    var followedUsers = mutableStateListOf<FollowedUser>()\n    var updating by mutableStateOf(true)\n\n    init {\n        viewModelScope.launch(Dispatchers.IO) {\n            initFollowedUsers()\n        }\n    }\n\n    private suspend fun initFollowedUsers() {\n        runCatching {\n            logger.fInfo { \"Init followed users\" }\n            val followedUserList = userRepository.getFollowedUsers(\n                mid = Prefs.uid,\n                preferApiType = Prefs.apiType\n            )\n            logger.fInfo { \"Followed user count: ${followedUserList.size}\" }\n            val sortedUserList = sortUsers(followedUserList)\n            followedUsers.swapListWithMainContext(sortedUserList)\n            logger.fInfo { \"Load followed user finish\" }\n        }\n        updating = false\n    }\n\n    private fun sortUsers(users: List<FollowedUser>): List<FollowedUser> {\n        val sortedList = mutableStateListOf<FollowedUser>()\n        val usersStartWithoutChinese =\n            users.filter { Regex(\"^[A-Za-z0-9_-]\").containsMatchIn(it.name) }\n                .toMutableList()\n        val usersStartWithChinese =\n            (users - usersStartWithoutChinese.toSet()).toMutableList()\n\n        usersStartWithoutChinese.sortWith { o1, o2 ->\n            Collator.getInstance(Locale.CHINA).compare(o1.name, o2.name)\n        }\n        usersStartWithChinese.sortWith { o1, o2 ->\n            Collator.getInstance(Locale.CHINA).compare(o1.name, o2.name)\n        }\n\n        logger.info { \"sorted user which start without chinese: ${usersStartWithoutChinese.map { it.name }}\" }\n        logger.info { \"sorted user which start with chinese: ${usersStartWithChinese.map { it.name }}\" }\n\n        sortedList.addAll(usersStartWithoutChinese)\n        sortedList.addAll(usersStartWithChinese)\n\n        return sortedList\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/user/FollowingSeasonViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.user\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeason\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonStatus\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonType\nimport dev.aaa1115910.biliapi.repositories.SeasonRepository\nimport dev.aaa1115910.biliapi.repositories.UserRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.fWarn\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass FollowingSeasonViewModel(\n    private val seasonRepository: SeasonRepository,\n    private val userRepository: UserRepository\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    val followingSeasons = mutableStateListOf<FollowingSeason>()\n    var followingSeasonType by mutableStateOf(FollowingSeasonType.Bangumi)\n    var followingSeasonStatus by mutableStateOf(FollowingSeasonStatus.All)\n\n    private var pageNumber = 1\n    private var pageSize = 30\n    var noMore by mutableStateOf(false)\n    var updating by mutableStateOf(false)\n\n//    init {\n//        followingSeasonType = FollowingSeasonType.Bangumi\n//        followingSeasonStatus = FollowingSeasonStatus.All\n//    }\n\n    fun clearData() {\n        pageNumber = 1\n        pageSize = 30\n        updating = false\n        noMore = false\n        followingSeasons.clear()\n    }\n\n    fun loadMore() {\n        viewModelScope.launch(Dispatchers.IO) {\n            updateData()\n        }\n    }\n\n    private suspend fun updateData() {\n        if (updating) return\n        withContext(Dispatchers.Main) {\n            updating = true\n        }\n        runCatching {\n            logger.fInfo { \"Updating following season data\" }\n            val response = seasonRepository.getFollowingSeasons(\n                type = followingSeasonType,\n                status = followingSeasonStatus,\n                pageNumber = pageNumber,\n                pageSize = pageSize,\n                preferApiType = Prefs.apiType\n            )\n            withContext(Dispatchers.Main) {\n                if (pageSize * pageNumber >= response.total) noMore = true\n                pageNumber++\n                followingSeasons.addAll(response.list)\n            }\n            logger.fInfo { \"Following season count: ${response.list.size}\" }\n        }.onFailure {\n            logger.fInfo { \"Update following seasons failed: ${it.stackTraceToString()}\" }\n        }\n        withContext(Dispatchers.Main) {\n            updating = false\n        }\n    }\n\n    var deleting by mutableStateOf(false)\n        private set\n\n    fun unfollowSeason(seasonId: Int) {\n        if (deleting) return\n        deleting = true\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                userRepository.delSeasonFollow(\n                    seasonId = seasonId,\n                    preferApiType = Prefs.apiType\n                )\n                withContext(Dispatchers.Main) {\n                    followingSeasons.removeAll { it.seasonId == seasonId }\n                }\n                logger.fInfo { \"Unfollow season success: seasonId=$seasonId\" }\n                withContext(Dispatchers.Main) {\n                    BVApp.context.getString(R.string.following_season_delete_success)\n                        .toast(BVApp.context)\n                }\n            }.onFailure {\n                logger.fWarn { \"Unfollow season failed: ${it.stackTraceToString()}\" }\n                withContext(Dispatchers.Main) {\n                    BVApp.context.getString(R.string.following_season_delete_failed)\n                        .toast(BVApp.context)\n                }\n            }\n            withContext(Dispatchers.Main) {\n                deleting = false\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/user/HistoryViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.user\n\nimport android.content.Context\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.ugc.toSmartDate\nimport dev.aaa1115910.biliapi.entity.user.HistoryItemType\nimport dev.aaa1115910.biliapi.http.entity.AuthFailureException\nimport dev.aaa1115910.biliapi.repositories.HistoryRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.repository.UserRepository\nimport dev.aaa1115910.bv.util.Prefs\n\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.fWarn\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass HistoryViewModel(\n    private val userRepository: UserRepository,\n    private val historyRepository: HistoryRepository\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    var histories by mutableStateOf<List<VideoCardData>>(emptyList())\n    var noMore by mutableStateOf(false)\n\n    private var cursor = 0L\n    var updating by mutableStateOf(false)\n    var deleting by mutableStateOf(false)\n        private set\n\n    fun update() {\n        viewModelScope.launch(Dispatchers.IO) {\n            updateHistories()\n        }\n    }\n\n    private suspend fun updateHistories(context: Context = BVApp.context) {\n        if (updating || noMore) return\n        logger.fInfo { \"Updating histories with params [cursor=$cursor, apiType=${Prefs.apiType}]\" }\n        withContext(Dispatchers.Main) {\n            updating = true\n        }\n        runCatching {\n            val data = historyRepository.getHistories(\n                cursor = cursor,\n                preferApiType = Prefs.apiType\n            )\n\n            val newItems = data.data.map { historyItem ->\n                val isPgc = historyItem.type == HistoryItemType.Pgc\n                VideoCardData(\n                    avid = historyItem.oid,\n                    title = historyItem.title,\n                    cover = historyItem.cover,\n                    upName = historyItem.author,\n                    upId = historyItem.authorId,\n                    upFace = historyItem.authorFace,\n                    timeString = if (historyItem.progress == -1) context.getString(R.string.play_time_finish)\n                    else context.getString(\n                        R.string.play_time_history,\n                        (historyItem.progress * 1000L).formatHourMinSec(),\n                        (historyItem.duration * 1000L).formatHourMinSec()\n                    ),\n                    jumpToSeason = isPgc,\n                    epId = historyItem.epid,\n                    seasonId = historyItem.seasonId ?: if (isPgc) historyItem.kid.toInt() else null,\n                    pubTime = historyItem.viewAt.toSmartDate() + context.getString(R.string.view_at),\n                    historyBusiness = when (historyItem.type) {\n                        HistoryItemType.Archive -> \"archive\"\n                        HistoryItemType.Pgc -> \"pgc\"\n                        HistoryItemType.Unknown -> null\n                    },\n                    historyKid = historyItem.kid\n                )\n            }\n            withContext(Dispatchers.Main) {\n                histories = histories + newItems\n            }\n            //update cursor\n            cursor = data.cursor\n            logger.fInfo { \"Update history cursor: [cursor=$cursor]\" }\n            logger.fInfo { \"Update histories success\" }\n            if (cursor == 0L) {\n                withContext(Dispatchers.Main) { noMore = true }\n                logger.fInfo { \"No more history\" }\n            }\n        }.onFailure {\n            logger.fWarn { \"Update histories failed: ${it.stackTraceToString()}\" }\n            when (it) {\n                is AuthFailureException -> {\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.exception_auth_failure)\n                            .toast(BVApp.context)\n                    }\n                    logger.fInfo { \"User auth failure\" }\n                    if (!BuildConfig.DEBUG) userRepository.logout()\n                }\n\n                else -> {}\n            }\n        }\n        withContext(Dispatchers.Main) {\n            updating = false\n        }\n    }\n\n    fun clearData() {\n        histories = emptyList()\n        cursor = 0L\n        noMore = false\n        logger.fInfo { \"History data cleared\" }\n    }\n\n    fun deleteHistory(business: String?, kid: Long?) {\n        if (deleting || business == null || kid == null) return\n        deleting = true\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                val success = historyRepository.deleteHistory(\n                    business = business,\n                    kid = kid,\n                    preferApiType = Prefs.apiType\n                )\n                if (success) {\n                    withContext(Dispatchers.Main) {\n                        histories = histories.filter { !(it.historyBusiness == business && it.historyKid == kid) }\n                    }\n                    logger.fInfo { \"Delete history success: business=$business, kid=$kid\" }\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.history_delete_success)\n                            .toast(BVApp.context)\n                    }\n                } else {\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.history_delete_failed)\n                            .toast(BVApp.context)\n                    }\n                }\n            }.onFailure {\n                logger.fWarn { \"Delete history failed: ${it.stackTraceToString()}\" }\n                withContext(Dispatchers.Main) {\n                    BVApp.context.getString(R.string.history_delete_failed)\n                        .toast(BVApp.context)\n                }\n            }\n            withContext(Dispatchers.Main) {\n                deleting = false\n            }\n        }\n    }\n\n    fun clearHistory() {\n        if (deleting) return\n        deleting = true\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                val success = historyRepository.clearHistory(\n                    preferApiType = Prefs.apiType\n                )\n                if (success) {\n                    withContext(Dispatchers.Main) {\n                        clearData()\n                        noMore = true\n                    }\n                    logger.fInfo { \"Clear history success\" }\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.history_clear_success)\n                            .toast(BVApp.context)\n                    }\n                } else {\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.history_clear_failed)\n                            .toast(BVApp.context)\n                    }\n                }\n            }.onFailure {\n                logger.fWarn { \"Clear history failed: ${it.stackTraceToString()}\" }\n                withContext(Dispatchers.Main) {\n                    BVApp.context.getString(R.string.history_clear_failed)\n                        .toast(BVApp.context)\n                }\n            }\n            withContext(Dispatchers.Main) {\n                deleting = false\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/user/ToViewViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.user\n\nimport android.content.Context\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.ugc.toSmartDate\nimport dev.aaa1115910.biliapi.http.entity.AuthFailureException\nimport dev.aaa1115910.biliapi.repositories.ToViewRepository\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.repository.UserRepository\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.fWarn\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass ToViewViewModel(\n    private val userRepository: UserRepository,\n    private val ToViewRepository: ToViewRepository\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    var histories by mutableStateOf<List<VideoCardData>>(emptyList())\n    var noMore by mutableStateOf(false)\n\n    private var cursor = 0L\n    private var updating = false\n    var deleting by mutableStateOf(false)\n        private set\n\n    fun update() {\n        viewModelScope.launch(Dispatchers.IO) {\n            updateToView()\n        }\n    }\n\n    private suspend fun updateToView(context: Context = BVApp.context) {\n        if (updating || noMore) return\n        logger.fInfo { \"Updating histories with params [cursor=$cursor, apiType=${Prefs.apiType}]\" }\n        withContext(Dispatchers.Main) {\n            updating = true\n        }\n        runCatching {\n            val data = ToViewRepository.getToView(\n                cursor = cursor,\n                preferApiType = Prefs.apiType\n            )\n\n            data.data.forEach { ToViewItem ->\n                val newCard = \n                    VideoCardData(\n                        avid = ToViewItem.oid,\n                        title = ToViewItem.title,\n                        cover = ToViewItem.cover,\n                        play = ToViewItem.play,\n                        // danmaku = ToViewItem.danmaku, // 视频时长>1小时时 显示不全，所以不显示弹幕数\n                        pubTime = ToViewItem.pubdate.toSmartDate(),\n                        upName = ToViewItem.author,\n                        upId = ToViewItem.authorId,\n                        upFace = ToViewItem.authorFace,\n                        timeString = if (ToViewItem.progress == -1) context.getString(R.string.play_time_finish)\n                        else context.getString(\n                            R.string.play_time_history,\n                            (ToViewItem.progress * 1000L).formatHourMinSec(),\n                            (ToViewItem.duration * 1000L).formatHourMinSec()\n                        )\n                    )\n                withContext(Dispatchers.Main) { histories = histories + newCard }\n            }\n            //update cursor\n            cursor = data.cursor\n            logger.fInfo { \"Update toview cursor: [cursor=$cursor]\" }\n            logger.fInfo { \"Update histories success\" }\n            if (cursor == 0L) {\n                withContext(Dispatchers.Main) { noMore = true }\n                logger.fInfo { \"No more toview\" }\n            }\n        }.onFailure {\n            logger.fWarn { \"Update histories failed: ${it.stackTraceToString()}\" }\n            when (it) {\n                is AuthFailureException -> {\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.exception_auth_failure)\n                            .toast(BVApp.context)\n                    }\n                    logger.fInfo { \"User auth failure\" }\n                    if (!BuildConfig.DEBUG) userRepository.logout()\n                }\n\n                else -> {}\n            }\n        }\n        withContext(Dispatchers.Main) {\n            updating = false\n        }\n    }\n\n    fun clearData() {\n        histories = emptyList()\n        cursor = 0L\n        noMore = false\n        logger.fInfo { \"ToView data cleared\" }\n    }\n\n    fun deleteToView(avid: Long) {\n        if (deleting) return\n        deleting = true\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                val success = ToViewRepository.deleteToView(\n                    avid = avid,\n                    preferApiType = Prefs.apiType\n                )\n                if (success) {\n                    withContext(Dispatchers.Main) {\n                        histories = histories.filter { it.avid != avid }\n                    }\n                    logger.fInfo { \"Delete toview success: avid=$avid\" }\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.toview_delete_success)\n                            .toast(BVApp.context)\n                    }\n                } else {\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.toview_delete_failed)\n                            .toast(BVApp.context)\n                    }\n                }\n            }.onFailure {\n                logger.fWarn { \"Delete toview failed: ${it.stackTraceToString()}\" }\n                withContext(Dispatchers.Main) {\n                    BVApp.context.getString(R.string.toview_delete_failed)\n                        .toast(BVApp.context)\n                }\n            }\n            withContext(Dispatchers.Main) {\n                deleting = false\n            }\n        }\n    }\n\n    fun clearToView() {\n        if (deleting) return\n        deleting = true\n        viewModelScope.launch(Dispatchers.IO) {\n            runCatching {\n                val success = ToViewRepository.clearToView(\n                    preferApiType = Prefs.apiType\n                )\n                if (success) {\n                    withContext(Dispatchers.Main) {\n                        clearData()\n                        noMore = true\n                    }\n                    logger.fInfo { \"Clear toview success\" }\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.toview_clear_success)\n                            .toast(BVApp.context)\n                    }\n                } else {\n                    withContext(Dispatchers.Main) {\n                        BVApp.context.getString(R.string.toview_clear_failed)\n                            .toast(BVApp.context)\n                    }\n                }\n            }.onFailure {\n                logger.fWarn { \"Clear toview failed: ${it.stackTraceToString()}\" }\n                withContext(Dispatchers.Main) {\n                    BVApp.context.getString(R.string.toview_clear_failed)\n                        .toast(BVApp.context)\n                }\n            }\n            withContext(Dispatchers.Main) {\n                deleting = false\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/user/UserSpaceViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.user\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport dev.aaa1115910.biliapi.entity.ugc.toSmartDate\nimport dev.aaa1115910.biliapi.entity.user.SpaceVideoPage\nimport dev.aaa1115910.biliapi.entity.user.SpaceVideo\nimport dev.aaa1115910.biliapi.repositories.UserRepository\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.addWithMainContext\nimport dev.aaa1115910.bv.util.fInfo\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass UserSpaceViewModel(\n    private val userRepository: UserRepository\n) : ViewModel() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n    }\n\n    var upFace by mutableStateOf(\"\")\n    var upName by mutableStateOf(\"\")\n    var sign by mutableStateOf(\"\") // 个性签名\n    var fans by mutableIntStateOf(0) // 粉丝数\n    var friend by mutableIntStateOf(0) // 关注数\n    var upMid by mutableLongStateOf(0L)\n    var tvSpaceVideos = mutableStateListOf<VideoCardData>()\n    var spaceVideos = mutableStateListOf<SpaceVideo>()\n\n    private var page = SpaceVideoPage()\n    private var updating = false\n    val noMore get() = !page.hasNext\n\n    fun update() {\n        viewModelScope.launch(Dispatchers.Default) {\n            updateSpaceVideos()\n        }\n    }\n\n    private suspend fun updateSpaceVideos() {\n        if (updating || noMore) return\n        logger.fInfo { \"Updating up [mid=$upMid] space videos from page $page\" }\n        updating = true\n        runCatching {\n            val spaceVideoData = userRepository.getSpaceVideos(\n                mid = upMid,\n                page = page,\n                preferApiType = Prefs.apiType\n            )\n            spaceVideos.addAll(spaceVideoData.videos)\n            spaceVideoData.videos.forEach { spaceVideoItem ->\n                tvSpaceVideos.addWithMainContext(\n                    VideoCardData(\n                        avid = spaceVideoItem.aid,\n                        title = spaceVideoItem.title,\n                        //TODO 这里在改造 app 端接口时，没找到在空间内显示为合集样式封面的UP,没法进一步测试接口\n                        cover = spaceVideoItem.cover,\n                        play = spaceVideoItem.play,\n                        danmaku = spaceVideoItem.danmaku,\n                        upName = spaceVideoItem.author,\n                        upId = spaceVideoItem.authorId,\n                        time = spaceVideoItem.duration * 1000L,\n                        pubTime = spaceVideoItem.publishDate.getTime().toSmartDate(),\n                        isChargingArc = spaceVideoItem.isChargingArc,\n                        badgeText = spaceVideoItem.chargingArcBadge\n                    )\n                )\n            }\n            page = spaceVideoData.page\n            logger.fInfo { \"Update up space videos success\" }\n        }.onFailure {\n            logger.fInfo { \"Update up space videos failed: ${it.stackTraceToString()}\" }\n        }\n        updating = false\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/video/VideoDetailViewModel.kt",
    "content": "package dev.aaa1115910.bv.viewmodel.video\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport dev.aaa1115910.biliapi.entity.video.VideoDetail\nimport dev.aaa1115910.biliapi.repositories.VideoDetailRepository\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.player.entity.VideoListItem\nimport dev.aaa1115910.bv.player.entity.VideoListPart\nimport dev.aaa1115910.bv.player.entity.VideoListUgcEpisode\nimport dev.aaa1115910.bv.player.entity.VideoListUgcEpisodeTitle\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.swapListWithMainContext\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.koin.android.annotation.KoinViewModel\n\n@KoinViewModel\nclass VideoDetailViewModel(\n    private val videoDetailRepository: VideoDetailRepository,\n    private val videoInfoRepository: VideoInfoRepository\n) : ViewModel() {\n    private val logger = KotlinLogging.logger { }\n    var state by mutableStateOf(VideoInfoState.Loading)\n    var videoDetail: VideoDetail? by mutableStateOf(null)\n\n    var relatedVideos = mutableStateListOf<VideoCardData>()\n\n    suspend fun loadDetail(aid: Long, fromPgcSeason: Boolean = false, withUserActions: Boolean = true) {\n        logger.fInfo { \"Load detail: [avid=$aid, preferApiType=${Prefs.apiType.name}]\" }\n        state = VideoInfoState.Loading\n        runCatching {\n            val videoDetailData = videoDetailRepository.getVideoDetail(\n                aid = aid,\n                preferApiType = Prefs.apiType,\n                withUserActions = withUserActions\n            )\n            withContext(Dispatchers.Main) { videoDetail = videoDetailData }\n            if (!fromPgcSeason) updateVideoList(aid)\n        }.onFailure {\n            state = VideoInfoState.Error\n            logger.fInfo { \"Load video av$aid failed: ${it.stackTraceToString()}\" }\n        }.onSuccess {\n            state = VideoInfoState.Success\n            logger.fInfo { \"Load video av$aid success\" }\n\n            updateRelatedVideos()\n        }.getOrThrow()\n    }\n\n    suspend fun loadDetailOnlyUpdateHistory(aid: Long) {\n        logger.fInfo { \"Load detail only update history: [avid=$aid, preferApiType=${Prefs.apiType.name}]\" }\n        runCatching {\n            val historyData = videoDetailRepository.getVideoDetail(\n                aid = aid,\n                preferApiType = Prefs.apiType,\n                withUserActions = false\n            ).history\n            withContext(Dispatchers.Main) { videoDetail?.history = historyData }\n        }.onFailure {\n            logger.fInfo { \"Load video av$aid only update history failed: ${it.stackTraceToString()}\" }\n        }.onSuccess {\n            logger.fInfo { \"Load video av$aid only update history success: ${videoDetail?.history}\" }\n        }\n    }\n\n    private suspend fun updateRelatedVideos() {\n        logger.fInfo { \"Start update relate video\" }\n        val relateVideoCardDataList = videoDetail?.relatedVideos?.map {\n            VideoCardData(\n                avid = it.aid,\n                title = it.title,\n                cover = it.cover,\n                upName = it.author?.name ?: \"\",\n                time = it.duration * 1000L,\n                play = it.view,\n                danmaku = it.danmaku,\n                jumpToSeason = it.jumpToSeason,\n                epId = it.epid,\n                pubTime = it.pubTime,\n                upId = it.author?.mid ?: 0,\n                upFace = it.author?.face ?: \"\",\n                isChargingArc = it.isChargingArchive\n            )\n        } ?: emptyList()\n        relatedVideos.swapListWithMainContext(relateVideoCardDataList)\n        logger.fInfo { \"Update ${relateVideoCardDataList.size} relate videos\" }\n    }\n\n    private fun updateVideoList(aid: Long) {\n        if (videoDetail?.ugcSeason != null) {\n            updateUgcSeasonSectionVideoList(0)\n        } else {\n            val partVideoList =\n                videoDetail!!.pages.mapIndexed { index, videoPage ->\n                    VideoListPart(\n                        aid = aid,\n                        cid = videoPage.cid,\n                        title = videoDetail!!.title,\n                        partTitle = videoPage.title,\n                        index = index,\n                        cover = videoDetail!!.cover,\n                        duration = videoPage.duration,\n                    )\n                }\n            videoInfoRepository.videoList.clear()\n            videoInfoRepository.videoList.addAll(partVideoList)\n        }\n    }\n\n    fun updateUgcSeasonSectionVideoList(sectionIndex: Int) {\n        val partVideoList = mutableListOf<VideoListItem>()\n        videoDetail!!.ugcSeason!!.sections[sectionIndex].episodes.mapIndexed { epIndex, episode ->\n            if (episode.pages.size == 1) {\n                episode.pages.mapIndexed { pageInd, videoPage ->\n                    partVideoList.add(\n                        VideoListUgcEpisode(\n                            aid = episode.aid,\n                            cid = videoPage.cid,\n                            title = episode.title,\n                            partTitle = \"\",\n                            index = epIndex,\n                            cover = episode.cover,\n                            duration = episode.duration,\n                            pubDate = episode.pubDate,\n                        )\n                    )\n                }\n            } else {\n                partVideoList.add(\n                    VideoListUgcEpisodeTitle(\n                        title = episode.title,\n                        index = epIndex,\n                    )\n                )\n                episode.pages.mapIndexed { pageIndex, videoPage ->\n                    partVideoList.add(\n                        VideoListPart(\n                            aid = episode.aid,\n                            cid = videoPage.cid,\n                            title = episode.title,\n                            partTitle = videoPage.title,\n                            index = pageIndex,\n                            cover = episode.cover,\n                            duration = videoPage.duration,\n                            pubDate = episode.pubDate,\n                        )\n                    )\n                }\n            }\n        }\n        videoInfoRepository.videoList.clear()\n        videoInfoRepository.videoList.addAll(partVideoList)\n    }\n}\n\nenum class VideoInfoState {\n    Loading,\n    Success,\n    Error\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/m3qrcode/DampedString.kt",
    "content": "package dev.aaa1115910.m3qrcode\n\nimport kotlin.math.abs\nimport kotlin.math.cos\nimport kotlin.math.exp\nimport kotlin.math.pow\nimport kotlin.math.sin\nimport kotlin.math.sqrt\n\nclass DampedString(period: Int, private val dampingRatio: Float) {\n    private val dampedNaturalFrequency: Float\n    private val stiffness: Float\n    private val undampedNaturalFrequency: Float\n\n    init {\n        stiffness = ((6.2831855f / period).toDouble().pow(2.0).toFloat()) * 1.0f\n        val sqrt = sqrt((stiffness / 1.0f).toDouble()).toFloat()\n        undampedNaturalFrequency = sqrt\n        dampedNaturalFrequency = sqrt * (sqrt(abs(1 - dampingRatio.pow(2f))))\n    }\n\n    fun calculatePosition(i: Int): Float {\n        val f = undampedNaturalFrequency * dampingRatio\n        val f2 = dampedNaturalFrequency\n        val f3 = ((f * (-1.0f)) + 0.0f) / f2\n        val d = (f2 * i).toDouble()\n        return ((exp(((-f) * i))) * ((f3 * (sin(d).toFloat())) + ((cos(d).toFloat()) * (-1.0f)))) + 1.0f\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/m3qrcode/EmphasizedInterpolator.kt",
    "content": "package dev.aaa1115910.m3qrcode\n\nimport android.graphics.Path;\nimport android.view.animation.PathInterpolator;\n\nobject EmphasizedInterpolator {\n    private val interpolator: PathInterpolator\n\n    init {\n        val path = Path().apply {\n            moveTo(0.0f, 0.0f)\n            cubicTo(0.05f, 0.0f, 0.133333f, 0.06f, 0.166666f, 0.4f)\n            cubicTo(0.208333f, 0.82f, 0.25f, 1.0f, 1.0f, 1.0f)\n        }\n        interpolator = PathInterpolator(path)\n    }\n\n    fun getInterpolation(input: Float): Float = interpolator.getInterpolation(input)\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/m3qrcode/EntryAnimationStyle.kt",
    "content": "package dev.aaa1115910.m3qrcode\n\nenum class EntryAnimationStyle {\n    None,\n    ZoomIn,\n    EmphasizedZoomIn,\n    SpringZoomIn,\n    RotateEmphasizedZoomIn;\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/m3qrcode/MaterialShapeQr.kt",
    "content": "package dev.aaa1115910.m3qrcode\n\nimport android.graphics.Canvas\nimport android.graphics.PorterDuff\nimport android.graphics.PorterDuffColorFilter\nimport android.os.Build\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.graphics.nativeCanvas\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalView\nimport androidx.core.graphics.withTranslation\nimport com.airbnb.lottie.LottieProperty\nimport com.airbnb.lottie.compose.LottieAnimation\nimport com.airbnb.lottie.compose.LottieCompositionSpec\nimport com.airbnb.lottie.compose.LottieConstants\nimport com.airbnb.lottie.compose.animateLottieCompositionAsState\nimport com.airbnb.lottie.compose.rememberLottieComposition\nimport com.airbnb.lottie.compose.rememberLottieDynamicProperties\nimport com.airbnb.lottie.compose.rememberLottieDynamicProperty\nimport dev.aaa1115910.bv.R\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlin.math.min\n\n@Composable\nfun MaterialShapeQr(\n    modifier: Modifier = Modifier,\n    content: String,\n    ecLevel: MaterialShapeQrErrorCorrectionLevel = MaterialShapeQrErrorCorrectionLevel.M,\n    colorScheme: ColorScheme? = null\n) {\n    val context = LocalContext.current\n    val view = LocalView.current\n    val state = rememberMaterialShapeQrState(\n        colorScheme = colorScheme ?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n            androidx.compose.material3.dynamicLightColorScheme(context)\n        } else {\n            androidx.compose.material3.lightColorScheme()\n        }\n    )\n    val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.lottie_qrcode_background))\n    val progress by animateLottieCompositionAsState(\n        composition = composition,\n        iterations = LottieConstants.IterateForever\n    )\n\n    val lottieDynamicProperties = rememberMaterialShapeQrLottieDynamicProperties(state.colorMap)\n\n    LaunchedEffect(content, ecLevel) {\n        state.updateContent(content, ecLevel.level)\n    }\n\n    // 每 16ms 更新一次（近似 60fps），当还未生成 QR 时降低频率为 50ms\n    LaunchedEffect(Unit) {\n        while (isActive) {\n            if (state.qrcodeLineCount == 0) {\n                delay(50)\n                continue\n            }\n            state.frameElapsed = System.currentTimeMillis() - state.qrStartTime\n            delay(16)\n        }\n    }\n\n    Box(\n        modifier = modifier\n            .aspectRatio(1f),\n        contentAlignment = Alignment.Center\n    ) {\n        LottieAnimation(\n            modifier = Modifier.scale(state.lottieScale),\n            composition = composition,\n            progress = { progress },\n            dynamicProperties = lottieDynamicProperties\n        )\n        if (view.isInEditMode) {\n            LottieAnimation(\n                composition = composition,\n                progress = { progress },\n                dynamicProperties = lottieDynamicProperties\n            )\n        }\n        Box(\n            modifier = Modifier\n                .fillMaxSize(0.8f)\n        ) {\n\n            val nonFinderFrameElapsed by remember {\n                derivedStateOf {\n                    if (state.hasFinalDataImage) 3000L else state.frameElapsed\n                }\n            }\n\n            NonFinderPatternsCanvas(\n                qrcodeLineCount = state.qrcodeLineCount,\n                qrcodeSize = state.qrcodeSize,\n                frameElapsed = nonFinderFrameElapsed,\n                dataModuleShapeList = state.dataModuleShapeList,\n                backgroundModuleShapeList = state.backgroundModuleShapeList,\n                hasFinalDataImage = state.hasFinalDataImage,\n                onHasFinalDataImageChange = { state.hasFinalDataImage = it },\n                onDrawAnimationBackground = { elapsedMs ->\n                    state.drawAnimationBackground(elapsedMs)\n                }\n            )\n            FinderPatternsCanvas(\n                qrcodeLineCount = state.qrcodeLineCount,\n                qrcodeSize = state.qrcodeSize,\n                frameElapsed = state.frameElapsed,\n                finderPatternShapeList = state.finderPatternShapeList\n            )\n        }\n    }\n}\n\n@Composable\nfun NonFinderPatternsCanvas(\n    modifier: Modifier = Modifier,\n    qrcodeLineCount: Int,\n    qrcodeSize: Int,\n    frameElapsed: Long,\n    dataModuleShapeList: List<MaterialShapeRenderer>,\n    backgroundModuleShapeList: List<MaterialShapeRenderer>,\n    hasFinalDataImage: Boolean = false,\n    onHasFinalDataImageChange: (Boolean) -> Unit,\n    onDrawAnimationBackground: (Long) -> Unit\n) {\n    Canvas(\n        modifier = modifier.fillMaxSize()\n    ) {\n        val native = drawContext.canvas.nativeCanvas\n\n        if (qrcodeLineCount == 0) return@Canvas\n\n        val canvasWidth = size.width\n        val canvasHeight = size.height\n\n        val scale = min(canvasWidth / qrcodeSize.toFloat(), canvasHeight / qrcodeSize.toFloat())\n        val scaledWidth = qrcodeSize * scale\n        val scaledHeight = qrcodeSize * scale\n\n        val tx = (canvasWidth - scaledWidth) / 2f\n        val ty = (canvasHeight - scaledHeight) / 2f\n\n        val elapsed = frameElapsed\n        if (elapsed > 3000L && !hasFinalDataImage) {\n            onHasFinalDataImageChange(true)\n        }\n        val drawElapsed = if (hasFinalDataImage) 3000L else elapsed\n\n        native.withTranslation(tx, ty) {\n            scale(scale, scale)\n            drawShapeListOnCanvas(dataModuleShapeList, this, drawElapsed)\n            drawShapeListOnCanvas(backgroundModuleShapeList, this, drawElapsed)\n        }\n\n        if (!hasFinalDataImage) {\n            onDrawAnimationBackground(drawElapsed)\n        }\n    }\n}\n\n@Composable\nfun FinderPatternsCanvas(\n    modifier: Modifier = Modifier,\n    qrcodeLineCount: Int,\n    qrcodeSize: Int,\n    frameElapsed: Long,\n    finderPatternShapeList: List<MaterialShapeRenderer>\n) {\n    Canvas(\n        modifier = modifier.fillMaxSize()\n    ) {\n        val native = drawContext.canvas.nativeCanvas\n\n        if (qrcodeLineCount == 0) return@Canvas\n\n        val canvasWidth = size.width\n        val canvasHeight = size.height\n\n        val scale = min(canvasWidth / qrcodeSize.toFloat(), canvasHeight / qrcodeSize.toFloat())\n        val scaledWidth = qrcodeSize * scale\n        val scaledHeight = qrcodeSize * scale\n\n        val tx = (canvasWidth - scaledWidth) / 2f\n        val ty = (canvasHeight - scaledHeight) / 2f\n\n        val elapsed = frameElapsed\n\n        native.withTranslation(tx, ty) {\n            scale(scale, scale)\n            drawShapeListOnCanvas(finderPatternShapeList, this, elapsed)\n        }\n    }\n}\n\n@Composable\nprivate fun rememberMaterialShapeQrLottieDynamicProperties(\n    colorMap: Map<String, Int>\n): com.airbnb.lottie.compose.LottieDynamicProperties {\n    val properties = colorMap.map { (key, color) ->\n        val filter = remember(color) { PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) }\n        rememberLottieDynamicProperty(\n            property = LottieProperty.COLOR_FILTER,\n            keyPath = arrayOf(\"**\", key, \"**\"),\n        ) {\n            filter\n        }\n    }.toTypedArray()\n    return rememberLottieDynamicProperties(*properties)\n}\n\nprivate fun drawShapeListOnCanvas(\n    list: List<MaterialShapeRenderer>,\n    canvas: Canvas,\n    elapsedMs: Long\n) {\n    list.forEach { it.draw(canvas, elapsedMs) }\n}\n"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/m3qrcode/MaterialShapeQrErrorCorrectionLevel.kt",
    "content": "package dev.aaa1115910.m3qrcode\n\nimport com.google.zxing.qrcode.decoder.ErrorCorrectionLevel\n\nenum class MaterialShapeQrErrorCorrectionLevel(val level: ErrorCorrectionLevel) {\n    L(ErrorCorrectionLevel.L),\n    M(ErrorCorrectionLevel.M),\n    Q(ErrorCorrectionLevel.Q),\n    H(ErrorCorrectionLevel.H)\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/m3qrcode/MaterialShapeQrState.kt",
    "content": "package dev.aaa1115910.m3qrcode\n\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.graphics.PorterDuff\nimport android.graphics.PorterDuffColorFilter\nimport android.graphics.RectF\nimport android.graphics.drawable.Drawable\nimport androidx.appcompat.content.res.AppCompatResources\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshots.SnapshotStateList\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.core.graphics.ColorUtils\nimport com.google.zxing.qrcode.decoder.ErrorCorrectionLevel\nimport com.google.zxing.qrcode.encoder.Encoder\nimport com.google.zxing.qrcode.encoder.QRCode\nimport dev.aaa1115910.bv.R\nimport kotlin.math.ceil\nimport kotlin.math.min\nimport kotlin.math.sqrt\nimport kotlin.random.Random\n\n\ninternal data class MaterialShapeQrState(\n    private val context: Context,\n    private val arrayOf1x1Shapes: List<Drawable>,\n    private val arrayOf1x1SemiCircleShapes: List<Drawable>,\n    private val arrayOf2x2Shapes: List<Drawable>,\n    private val arrayOf3x3Shapes: List<Drawable>,\n    private val arrayOf7x7Shapes: List<Drawable>,\n    private val arrayOfHorizontalBarShapes: List<Drawable>,\n    private val arrayOfHorizontalHalfCapsuleBarShapes: List<Drawable>,\n    private val arrayOfVerticalBarShapes: List<Drawable>,\n    private val arrayOfFinderPatternCenterShapes: List<Drawable>,\n    private val foregroundColorPrimary: Int,\n    private val foregroundColorSecondary: Int,\n    private val foregroundColorAccent: Int,\n    private val backgroundShapeColor: Int,\n    private val backgroundDotColor1: Int,\n    private val backgroundDotColor2: Int,\n    private val backgroundDotColor3: Int,\n    private val mainForegroundColorArray: List<Int>,\n    private val mainBackgroundColorArray: List<Int>,\n    val colorMap: MutableMap<String, Int>\n\n) {\n    val finderPatternShapeList = mutableStateListOf<MaterialShapeRenderer>()\n    val dataModuleShapeList = mutableStateListOf<MaterialShapeRenderer>()\n    val backgroundModuleShapeList = mutableStateListOf<MaterialShapeRenderer>()\n    val hasCreated: SnapshotStateList<SnapshotStateList<Boolean>> = mutableStateListOf()\n\n    var finderPatternCenterShapeIndex by mutableIntStateOf(0)\n\n    var hasFinalDataImage by mutableStateOf(false)\n    var qrStartTime by mutableLongStateOf(0L)\n    var qrcode by mutableStateOf<QRCode?>(null)\n    var qrcodeSize by mutableIntStateOf(0)\n    var qrcodeLineCount by mutableIntStateOf(0)\n    var moduleSize by mutableIntStateOf(29)\n\n\n    var frameElapsed by mutableLongStateOf(0L)\n    var lottieScale by mutableFloatStateOf(0f)\n\n\n    fun getColorForFinderPattern(): Int {\n        return foregroundColorPrimary\n    }\n\n    fun calculateAnimationBackgroundAlpha(elapsedMs: Long): Float {\n        return when {\n            elapsedMs < 250 -> 0.0f\n            elapsedMs < 583 -> (elapsedMs - 250).toFloat() / 333\n            else -> 1.0f\n        }\n    }\n\n    fun calculateAnimationBackgroundScale(elapsedMs: Long): Float {\n        // 根据时间 elapsedMs 计算背景动画的缩放比例\n        if (elapsedMs < 250L) return 0.0f\n        if (elapsedMs >= 1083L) return 1.0f\n        val t = (elapsedMs - 250L).toFloat() / 833f\n        return (EmphasizedInterpolator.getInterpolation(t) * 0.19999999f) + 0.8f\n    }\n\n    fun drawAnimationBackground(elapsedMs: Long) {\n        // 当动画时间小于 1100ms 时，根据时间计算并设置背景动画的缩放\n        if (elapsedMs < 1100L) {\n            lottieScale = calculateAnimationBackgroundScale(elapsedMs)\n        }\n\n        // 在前 600ms 内根据计算的 alpha 值更新 Lottie 动画的背景颜色滤镜\n        if (elapsedMs <= 600L) {\n            val alphaFraction = calculateAnimationBackgroundAlpha(elapsedMs)\n            // 将浮点 alpha 转为 0..255 的整型并应用到 backgroundShapeColor\n            val alphaColor = ColorUtils.setAlphaComponent(\n                backgroundShapeColor,\n                (alphaFraction * 255).toInt()\n            )\n\n            colorMap[\".bg\"] = alphaColor\n        }\n    }\n\n    fun randomSquare(shapeSize: Int): Drawable {\n        return when (shapeSize) {\n            1 -> arrayOf1x1Shapes.random()\n            2 -> arrayOf2x2Shapes.random()\n            3 -> arrayOf3x3Shapes.random()\n            7 -> arrayOf7x7Shapes.first()\n            else -> throw IllegalArgumentException(\"Unsupported square shape: $shapeSize\")\n        }\n    }\n\n    fun randomHorizontalBar(barWidth: Int): Drawable {\n        return arrayOfHorizontalBarShapes[barWidth - 2]\n    }\n\n    fun randomHorizontalHalfCapsuleBar(barWidth: Int): Drawable {\n        return arrayOfHorizontalHalfCapsuleBarShapes[barWidth - 2]\n    }\n\n    fun randomVerticalBar(barHeight: Int): Drawable {\n        return arrayOfVerticalBarShapes[barHeight - 2]\n    }\n\n    fun getSemiCircle(): Drawable {\n        return arrayOf1x1SemiCircleShapes.first()\n    }\n\n    fun nextFinderPatternCenter(): Drawable {\n        val vectorDrawableArr = arrayOfFinderPatternCenterShapes\n        val index = finderPatternCenterShapeIndex\n        finderPatternCenterShapeIndex = index + 1\n        return vectorDrawableArr[index % vectorDrawableArr.size]\n    }\n\n    fun drawShapeListOnCanvas(list: List<MaterialShapeRenderer>, canvas: Canvas, elapsedMs: Long) {\n        list.forEach { it.draw(canvas, elapsedMs) }\n    }\n\n    fun randomRotationForSquareShape(): Int {\n        return Random.nextInt() % 4\n    }\n\n    fun randomForegroundColor(width: Int, height: Int): Int {\n        if (height == 1) {\n            val probs = listOf(0.2f, 0.04f, 0.01f, 0.0f)\n            val prob = probs.getOrNull(width - 1) ?: 0f\n            if (Random.nextFloat() <= prob) return foregroundColorAccent\n        }\n        return mainForegroundColorArray.random()\n    }\n\n    fun randomBackgroundColor(): Int {\n        return mainBackgroundColorArray.random()\n    }\n\n    fun calculateRatioToCenter(x: Int, y: Int, width: Int, height: Int): Float {\n        val halfLineCount = qrcodeLineCount / 2.0f\n        val dx = (x + (width / 2.0f)) - halfLineCount\n        val dy = (y + (height / 2.0f)) - halfLineCount\n        val distance = sqrt((dx * dx) + (dy * dy))\n        val maxDistance = 1.414f * halfLineCount\n        return distance / maxDistance\n    }\n\n    fun calculateStartDelay(x: Int, y: Int, width: Int, height: Int): Long {\n        val base = (calculateRatioToCenter(x, y, width, height) * 1000f).toLong()\n        val extra = if (width == 1 && height == 1) 0L else Random.Default.nextLong(400L)\n        return base + extra\n    }\n\n    fun markAsCreated(x: Int, y: Int, width: Int, height: Int) {\n        for (dx in 0 until width) {\n            for (dy in 0 until height) {\n                hasCreated[x + dx][y + dy] = true\n            }\n        }\n    }\n\n    fun isForeground(x: Int, y: Int): Boolean {\n        return (qrcode?.matrix?.get(x, y)?.toInt()?.and(15)) == 1\n    }\n\n    fun updateContent(\n        content: String,\n        errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.L\n    ) {\n        val encode = Encoder.encode(content, errorCorrectionLevel, null)\n        qrcode = encode\n        qrStartTime = System.currentTimeMillis()\n        hasFinalDataImage = false\n\n        // 获取矩阵宽度\n        qrcodeLineCount = qrcode?.matrix?.width ?: 0\n\n        // 计算 moduleSize（模块尺寸），上限 29\n        val displayMetrics = context.resources.displayMetrics\n        val screenMin = min(displayMetrics.widthPixels, displayMetrics.heightPixels)\n            .coerceAtMost(1200)\n        val candidate =\n            ceil(screenMin.toDouble() / qrcodeLineCount.toDouble()).toInt()\n        moduleSize = min(candidate, 29)\n\n        // 计算最终位图尺寸\n        qrcodeSize = moduleSize * qrcodeLineCount\n\n        hasCreated.clear()\n        for (i in 0 until qrcodeLineCount) {\n            val row = mutableStateListOf<Boolean>()\n            for (j in 0 until qrcodeLineCount) {\n                row.add(false)\n            }\n            hasCreated.add(row)\n        }\n\n        // 清空渲染器列表\n        finderPatternShapeList.clear()\n        dataModuleShapeList.clear()\n        backgroundModuleShapeList.clear()\n\n        val colorForFinderPattern = getColorForFinderPattern()\n\n        // 创建 3 个 finder patterns（左上、右上、左下）\n        createRendererForShape(\n            0, 0, 7, 7,\n            finderPatternShapeList, randomSquare(7), colorForFinderPattern\n        ) { renderer ->\n            renderer.animationStyle = EntryAnimationStyle.EmphasizedZoomIn\n            renderer.startDelay = 833\n            renderer.duration = 834\n        }\n        createRendererForShape(\n            qrcodeLineCount - 7, 0, 7, 7,\n            finderPatternShapeList, randomSquare(7), colorForFinderPattern\n        ) { renderer ->\n            renderer.animationStyle = EntryAnimationStyle.EmphasizedZoomIn\n            renderer.startDelay = 833\n            renderer.duration = 834\n        }\n        createRendererForShape(\n            0, qrcodeLineCount - 7, 7, 7,\n            finderPatternShapeList, randomSquare(7), colorForFinderPattern\n        ) { renderer ->\n            renderer.animationStyle = EntryAnimationStyle.EmphasizedZoomIn\n            renderer.startDelay = 833\n            renderer.duration = 834\n        }\n\n        finderPatternCenterShapeIndex = 0\n\n        // Finder pattern centers (3x3 centers inside finder)\n        createRendererForShape(\n            2, 2, 3, 3,\n            finderPatternShapeList, nextFinderPatternCenter(), colorForFinderPattern\n        ) { renderer ->\n            renderer.animationStyle = EntryAnimationStyle.RotateEmphasizedZoomIn\n            renderer.startDelay = 1167\n            renderer.duration = 1667\n        }\n        createRendererForShape(\n            qrcodeLineCount - 5, 2, 3, 3,\n            finderPatternShapeList, nextFinderPatternCenter(), colorForFinderPattern\n        ) { renderer ->\n            renderer.animationStyle = EntryAnimationStyle.RotateEmphasizedZoomIn\n            renderer.startDelay = 1167\n            renderer.duration = 1667\n        }\n        createRendererForShape(\n            2, qrcodeLineCount - 5, 3, 3,\n            finderPatternShapeList, nextFinderPatternCenter(), colorForFinderPattern\n        ) { renderer ->\n            renderer.animationStyle = EntryAnimationStyle.RotateEmphasizedZoomIn\n            renderer.startDelay = 1167\n            renderer.duration = 1667\n        }\n\n        // 扫描并创建不同尺寸的前景/背景形状\n        searchAndCreateLargeSquareShapes(3, dataModuleShapeList)\n        searchAndCreateLargeSquareShapes(2, dataModuleShapeList)\n        searchAndCreateHorizontalBars(4, dataModuleShapeList)\n        searchAndCreateBars(3, dataModuleShapeList)\n        searchAndCreateBars(2, dataModuleShapeList)\n        searchAndCreateSmallForegroundSquareShapes(dataModuleShapeList)\n        searchAndCreateSmallBackgroundSquareShapes(backgroundModuleShapeList)\n    }\n\n\n    fun createRendererForShape(\n        x: Int,\n        y: Int,\n        width: Int,\n        height: Int,\n        list: MutableList<MaterialShapeRenderer>,\n        vectorDrawable: Drawable,\n        foregroundColor: Int,\n        rendererConfigs: (MaterialShapeRenderer) -> Unit\n    ) {\n        val rectF = RectF(\n            (x * moduleSize).toFloat(),\n            (y * moduleSize).toFloat(),\n            ((x + width) * moduleSize).toFloat(),\n            ((y + height) * moduleSize).toFloat()\n        )\n        val paint = Paint()\n        paint.colorFilter = PorterDuffColorFilter(foregroundColor, PorterDuff.Mode.SRC_IN)\n        val materialShapeRenderer = MaterialShapeRenderer(vectorDrawable, rectF, paint)\n        materialShapeRenderer.startDelay = calculateStartDelay(x, y, width, height)\n        rendererConfigs(materialShapeRenderer)\n        list.add(materialShapeRenderer)\n        markAsCreated(x, y, width, height)\n    }\n\n    fun createRendererForShape(\n        x: Int,\n        y: Int,\n        width: Int,\n        height: Int,\n        list: MutableList<MaterialShapeRenderer>,\n        vectorDrawable: Drawable,\n        rendererConfigs: (MaterialShapeRenderer) -> Unit\n    ) {\n        createRendererForShape(\n            x = x,\n            y = y,\n            width = width,\n            height = height,\n            list = list,\n            vectorDrawable = vectorDrawable,\n            foregroundColor = randomForegroundColor(width, height),\n            rendererConfigs = rendererConfigs\n        )\n    }\n\n    fun createSingleVerticalBar(\n        x: Int,\n        y: Int,\n        height: Int,\n        list: MutableList<MaterialShapeRenderer>\n    ) {\n        createRendererForShape(\n            x = x,\n            y = y,\n            width = 1,\n            height = height,\n            list = list,\n            vectorDrawable = randomVerticalBar(height)\n        ) { renderer: MaterialShapeRenderer ->\n            renderer.animationStyle = EntryAnimationStyle.SpringZoomIn\n            renderer.startDelay = calculateStartDelay(x, y, 1, height)\n        }\n    }\n\n    fun createHorizontalBar(\n        x: Int,\n        y: Int,\n        width: Int,\n        list: MutableList<MaterialShapeRenderer>\n    ) {\n        if (width > 4) {\n            throw IllegalArgumentException(\"barLen must be <= 4\")\n        }\n        if (width <= 2 || (width == 3 && Random.Default.nextFloat() < 0.5f)) {\n            createRendererForShape(\n                x, y, width, 1, list, randomHorizontalBar(width)\n            ) { renderer ->\n                renderer.animationStyle = EntryAnimationStyle.SpringZoomIn\n                renderer.startDelay = calculateStartDelay(x, y, width, 1)\n            }\n            return\n        }\n        if (Random.Default.nextFloat() > 0.5f) {\n            createRendererForShape(\n                x, y, 1, 1, list, getSemiCircle()\n            ) { renderer ->\n                renderer.animationStyle = EntryAnimationStyle.SpringZoomIn\n                renderer.startDelay = calculateStartDelay(x, y, 1, 1)\n                renderer.skipStartProgress = 0.3f\n            }\n            val longWidth = width - 1\n            createRendererForShape(\n                x + 1, y, longWidth, 1, list, randomHorizontalHalfCapsuleBar(longWidth)\n            ) { renderer ->\n                renderer.animationStyle = EntryAnimationStyle.SpringZoomIn\n                renderer.startDelay = calculateStartDelay(x + 1, y, width - 1, 1)\n            }\n        } else {\n            val longWidth = width - 1\n            createRendererForShape(\n                x, y, longWidth, 1, list, randomHorizontalHalfCapsuleBar(longWidth)\n            ) { renderer ->\n                renderer.initialRotation = 2\n                renderer.animationStyle = EntryAnimationStyle.SpringZoomIn\n                renderer.startDelay = calculateStartDelay(x, y, width - 1, 1)\n            }\n            createRendererForShape(\n                (x + width) - 1, y, 1, 1, list, getSemiCircle(), randomForegroundColor(1, 1)\n            ) { renderer ->\n                renderer.initialRotation = 2\n                renderer.animationStyle = EntryAnimationStyle.SpringZoomIn\n                renderer.startDelay = calculateStartDelay((x + y) - 1, width, 1, 1)\n                renderer.skipStartProgress = 0.3f\n            }\n        }\n    }\n\n    fun tryFindingHorizontalBar(\n        startX: Int,\n        startY: Int,\n        width: Int,\n        list: MutableList<MaterialShapeRenderer>\n    ) {\n        if (width > 4) throw IllegalArgumentException(\"barLen must be <= 4\")\n        val endX = startX + width\n        val n = qrcodeLineCount\n        if (endX > n || startY + 1 > n) return\n        for (k in 0 until width) {\n            val x = startX + k\n            if (hasCreated[x][startY] || !isForeground(x, startY)) return\n        }\n        createHorizontalBar(startX, startY, width, list)\n    }\n\n    fun tryFindingVerticalBar(\n        startX: Int,\n        startY: Int,\n        height: Int,\n        list: MutableList<MaterialShapeRenderer>\n    ) {\n        val endY = startY + height\n        val n = qrcodeLineCount\n        if (endY > n || startX + 1 > n) return\n\n        for (dy in 0 until height) {\n            val y = startY + dy\n            if (!isForeground(startX, y) || hasCreated[startX][y]) return\n        }\n\n        createSingleVerticalBar(startX, startY, height, list)\n    }\n\n    fun searchAndCreateSmallBackgroundSquareShapes(list: MutableList<MaterialShapeRenderer>) {\n        for (y in 0 until qrcodeLineCount) {\n            for (x in 0 until qrcodeLineCount) {\n                // 如果已经创建或是前景则跳过；否则为背景模块创建 1x1 方形渲染器\n                if (!hasCreated[x][y] && !isForeground(x, y)) {\n                    createRendererForShape(\n                        x, y, 1, 1, list, randomSquare(1)\n                    ) { renderer ->\n                        renderer.startDelay = calculateStartDelay(x, y, 1, 1)\n                        renderer.animationStyle = EntryAnimationStyle.EmphasizedZoomIn\n                        renderer.startDelay =\n                            83 + (1250 * calculateRatioToCenter(x, y, 1, 1)).toLong()\n                        renderer.skipStartProgress = 0.3f\n                        val randomBackgroundColor = randomBackgroundColor()\n                        val paint = Paint()\n                        paint.colorFilter =\n                            PorterDuffColorFilter(randomBackgroundColor, PorterDuff.Mode.SRC_IN)\n                        renderer.paint = paint\n                    }\n                }\n            }\n        }\n    }\n\n    fun searchAndCreateSmallForegroundSquareShapes(list: MutableList<MaterialShapeRenderer>) {\n        for (y in 0 until qrcodeLineCount) {\n            for (x in 0 until qrcodeLineCount) {\n                if (!hasCreated[x][y] && isForeground(x, y)) {\n                    createRendererForShape(\n                        x, y, 1, 1, list, randomSquare(1)\n                    ) { renderer ->\n                        renderer.animationStyle = EntryAnimationStyle.ZoomIn\n                        renderer.startDelay = calculateStartDelay(x, y, 1, 1)\n                        renderer.skipStartProgress = 0.3f\n                    }\n                }\n            }\n        }\n    }\n\n    fun searchAndCreateLargeSquareShapes(\n        len: Int,\n        list: MutableList<MaterialShapeRenderer>\n    ) {\n        val max = (qrcodeLineCount - len) + 1\n        for (y in 0 until max) {\n            for (x in 0 until max) {\n                if (!hasCreated[x][y] && isForeground(x, y)) {\n                    var ok = true\n                    for (dy in 0 until len) {\n                        for (dx in 0 until len) {\n                            val nx = x + dx\n                            val ny = y + dy\n                            if (hasCreated[nx][ny] || !isForeground(nx, ny)) {\n                                ok = false\n                                break\n                            }\n                        }\n                        if (!ok) break\n                    }\n                    if (ok) {\n                        createRendererForShape(\n                            x, y, len, len, list, randomSquare(len)\n                        ) { renderer ->\n                            renderer.animationStyle = EntryAnimationStyle.SpringZoomIn\n                            renderer.startDelay = calculateStartDelay(x, y, len, len)\n                            renderer.initialRotation = randomRotationForSquareShape()\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    fun searchAndCreateBars(len: Int, list: MutableList<MaterialShapeRenderer>) {\n        val funcs: List<(Int, Int, Int, MutableList<MaterialShapeRenderer>) -> Unit> =\n            listOf(\n                { x, y, l, lst -> tryFindingHorizontalBar(x, y, l, lst) },\n                { x, y, l, lst -> tryFindingVerticalBar(x, y, l, lst) }\n            )\n        val reversed = funcs.reversed()\n        val n = qrcodeLineCount\n        for (y in 0 until n) {\n            for (x in 0 until n) {\n                if (!hasCreated[x][y] && isForeground(x, y)) {\n                    val order = if (Random.Default.nextFloat() < 0.5f) funcs else reversed\n                    for (f in order) f(x, y, len, list)\n                }\n            }\n        }\n    }\n\n    fun searchAndCreateHorizontalBars(len: Int, list: MutableList<MaterialShapeRenderer>) {\n        if (len > 4) throw IllegalArgumentException(\"barLen must be <= 4\")\n        for (y in 0 until qrcodeLineCount) {\n            val maxX = (qrcodeLineCount - len) + 1\n            for (x in 0 until maxX) {\n                if (!hasCreated[x][y] && isForeground(x, y)) {\n                    var ok = true\n                    for (dx in 1 until len) {\n                        val nx = x + dx\n                        if (hasCreated[nx][y] || !isForeground(nx, y)) {\n                            ok = false\n                            break\n                        }\n                    }\n                    if (ok) {\n                        createHorizontalBar(x, y, len, list)\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\ninternal fun rememberMaterialShapeQrState(\n    context: Context = LocalContext.current,\n    colorScheme: ColorScheme\n): MaterialShapeQrState {\n    val foregroundColorPrimary = colorScheme.primary.toArgb()\n    val foregroundColorSecondary = colorScheme.secondary.toArgb()\n    val foregroundColorAccent = colorScheme.tertiary.toArgb()\n\n    val backgroundShapeColor = colorScheme.surfaceContainerHighest.toArgb()\n    val backgroundDotColor1 = colorScheme.primary.copy(38 / 255f).toArgb()\n    val backgroundDotColor2 = colorScheme.onSurface.copy(25 / 255f).toArgb()\n    val backgroundDotColor3 = colorScheme.tertiaryContainer.copy(76 / 255f).toArgb()\n\n    val mainForegroundColorArray by remember {\n        derivedStateOf {\n            listOf(foregroundColorPrimary, foregroundColorSecondary)\n        }\n    }\n    val mainBackgroundColorArray by remember {\n        derivedStateOf {\n            listOf(backgroundDotColor1, backgroundDotColor2, backgroundDotColor3)\n        }\n    }\n\n    val arrayOf1x1Shapes = remember { mutableStateListOf<Drawable>() }\n    val arrayOf1x1SemiCircleShapes = remember { mutableStateListOf<Drawable>() }\n    val arrayOf2x2Shapes = remember { mutableStateListOf<Drawable>() }\n    val arrayOf3x3Shapes = remember { mutableStateListOf<Drawable>() }\n    val arrayOf7x7Shapes = remember { mutableStateListOf<Drawable>() }\n\n    val arrayOfHorizontalBarShapes = remember { mutableStateListOf<Drawable>() }\n    val arrayOfHorizontalHalfCapsuleBarShapes = remember { mutableStateListOf<Drawable>() }\n    val arrayOfVerticalBarShapes = remember { mutableStateListOf<Drawable>() }\n    val arrayOfFinderPatternCenterShapes = remember { mutableStateListOf<Drawable>() }\n\n    val colorMap = remember { mutableStateMapOf<String, Int>() }\n\n    fun loadVectorDrawable(resId: Int): Drawable {\n        return AppCompatResources.getDrawable(context, resId)!!\n    }\n\n    fun loadDrawables() {\n        val s1Circle = loadVectorDrawable(R.drawable.qrcode_square_s1_circle)\n        val s1Drop = loadVectorDrawable(R.drawable.qrcode_square_s1_drop)\n        val s1SemiCircle = loadVectorDrawable(R.drawable.qrcode_square_s1_semi_circle)\n        val s1Square = loadVectorDrawable(R.drawable.qrcode_square_s1_square)\n\n        arrayOf1x1Shapes.addAll(listOf(s1Circle, s1Drop, s1Square))\n        arrayOf1x1SemiCircleShapes.addAll(listOf(s1SemiCircle))\n\n        arrayOf2x2Shapes.addAll(\n            listOf(\n                loadVectorDrawable(R.drawable.qrcode_square_s2_circle),\n                loadVectorDrawable(R.drawable.qrcode_square_s2_clover),\n                loadVectorDrawable(R.drawable.qrcode_square_s2_hexagonal),\n                loadVectorDrawable(R.drawable.qrcode_square_s2_meteroid),\n                loadVectorDrawable(R.drawable.qrcode_square_s2_wiggle_star)\n            )\n        )\n\n        val s3Circle = loadVectorDrawable(R.drawable.qrcode_square_s3_circle)\n        val s3Clover = loadVectorDrawable(R.drawable.qrcode_square_s3_clover)\n        val s3Hexagonal = loadVectorDrawable(R.drawable.qrcode_square_s3_hexagonal)\n        val s3Meteroid = loadVectorDrawable(R.drawable.qrcode_square_s3_meteroid)\n        val s3WiggleStar = loadVectorDrawable(R.drawable.qrcode_square_s3_wiggle_star)\n\n        arrayOf3x3Shapes.addAll(\n            listOf(s3Circle, s3Clover, s3Hexagonal, s3Meteroid, s3WiggleStar)\n        )\n        arrayOfFinderPatternCenterShapes.addAll(\n            listOf(s3Hexagonal, s3Meteroid, s3WiggleStar).shuffled()\n        )\n\n        arrayOf7x7Shapes.addAll(\n            listOf(loadVectorDrawable(R.drawable.qrcode_square_s7_ring))\n        )\n\n        val horBarS2Capsule = loadVectorDrawable(R.drawable.qrcode_hor_bar_s2_capsule)\n        val horBarS3Capsule = loadVectorDrawable(R.drawable.qrcode_hor_bar_s3_capsule)\n        val horBarS2HalfCapsule = loadVectorDrawable(R.drawable.qrcode_hor_bar_s2_half_capsule)\n        val horBarS3HalfCapsule = loadVectorDrawable(R.drawable.qrcode_hor_bar_s3_half_capsule)\n        val verBarS2Capsule = loadVectorDrawable(R.drawable.qrcode_ver_bar_s2_capsule)\n        val verBarS3Capsule = loadVectorDrawable(R.drawable.qrcode_ver_bar_s3_capsule)\n\n        arrayOfHorizontalBarShapes.addAll(\n            listOf(horBarS2Capsule, horBarS3Capsule)\n        )\n        arrayOfHorizontalHalfCapsuleBarShapes.addAll(\n            listOf(horBarS2HalfCapsule, horBarS3HalfCapsule)\n        )\n        arrayOfVerticalBarShapes.addAll(\n            listOf(verBarS2Capsule, verBarS3Capsule)\n        )\n    }\n\n    fun applyLottieDynamicColor() {\n        colorMap.clear()\n        colorMap[\".bg\"] = backgroundShapeColor\n        colorMap[\".dot1\"] = backgroundDotColor1\n        colorMap[\".dot2\"] = backgroundDotColor2\n        colorMap[\".dot3\"] = backgroundDotColor3\n    }\n\n    LaunchedEffect(Unit) {\n        loadDrawables()\n        applyLottieDynamicColor()\n    }\n\n    return remember(\n        context,\n    ) {\n        MaterialShapeQrState(\n            context = context,\n            foregroundColorPrimary = foregroundColorPrimary,\n            foregroundColorSecondary = foregroundColorSecondary,\n            foregroundColorAccent = foregroundColorAccent,\n            backgroundShapeColor = backgroundShapeColor,\n            backgroundDotColor1 = backgroundDotColor1,\n            backgroundDotColor2 = backgroundDotColor2,\n            backgroundDotColor3 = backgroundDotColor3,\n            mainForegroundColorArray = mainForegroundColorArray,\n            mainBackgroundColorArray = mainBackgroundColorArray,\n            arrayOf1x1Shapes = arrayOf1x1Shapes,\n            arrayOf1x1SemiCircleShapes = arrayOf1x1SemiCircleShapes,\n            arrayOf2x2Shapes = arrayOf2x2Shapes,\n            arrayOf3x3Shapes = arrayOf3x3Shapes,\n            arrayOf7x7Shapes = arrayOf7x7Shapes,\n            arrayOfHorizontalBarShapes = arrayOfHorizontalBarShapes,\n            arrayOfHorizontalHalfCapsuleBarShapes = arrayOfHorizontalHalfCapsuleBarShapes,\n            arrayOfVerticalBarShapes = arrayOfVerticalBarShapes,\n            arrayOfFinderPatternCenterShapes = arrayOfFinderPatternCenterShapes,\n            colorMap = colorMap\n        )\n    }\n}"
  },
  {
    "path": "app/shared/src/main/kotlin/dev/aaa1115910/m3qrcode/MaterialShapeRenderer.kt",
    "content": "package dev.aaa1115910.m3qrcode\n\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.graphics.Rect\nimport android.graphics.RectF\nimport android.graphics.drawable.Drawable\nimport android.os.Build\nimport androidx.core.graphics.withRotation\nimport androidx.core.graphics.withSave\nimport kotlin.math.cos\n\nclass MaterialShapeRenderer {\n    var animationStyle: EntryAnimationStyle\n    var destRect: RectF\n    var duration: Long = 0\n    var initialRotation: Int = 0\n    var isMotionPaused: Boolean = false\n    var paint: Paint\n    var skipStartProgress: Float = 0f\n    var srcImgSvg: Drawable\n    var startDelay: Long = 0\n\n    companion object {\n        private var springScaleCache: LinkedHashMap<Int, Float> = linkedMapOf()\n        private var dampedString: DampedString = DampedString(60, 0.63f)\n        fun calculateSpringScale(j: Long): Float {\n            val i = (j / 16).toInt()\n            if (springScaleCache.containsKey(i)) {\n                return springScaleCache[i]!!\n            }\n            val calculatePosition = dampedString.calculatePosition(i)\n            springScaleCache[i] = calculatePosition\n            return calculatePosition\n        }\n    }\n\n    constructor(srcImgSvg: Drawable, destRect: RectF, paint: Paint) {\n        this.srcImgSvg = srcImgSvg\n        this.destRect = destRect\n        this.paint = paint\n        this.animationStyle = EntryAnimationStyle.None\n    }\n\n    fun draw(canvas: Canvas, elapsedMs: Long) {\n        val delay = startDelay\n        if (elapsedMs < delay) return\n        val adjustedMs = elapsedMs - delay\n        when (animationStyle) {\n            EntryAnimationStyle.None -> drawForNone(canvas, adjustedMs)\n            EntryAnimationStyle.ZoomIn -> drawForZoomIn(canvas, adjustedMs)\n            EntryAnimationStyle.SpringZoomIn -> drawForSpringZoomIn(canvas, adjustedMs)\n            EntryAnimationStyle.RotateEmphasizedZoomIn -> drawForRotateEmphasizedZoomIn(\n                canvas,\n                adjustedMs\n            )\n\n            EntryAnimationStyle.EmphasizedZoomIn -> drawForEmphasizedZoomIn(canvas, adjustedMs)\n        }\n    }\n\n    private fun draw(canvas: Canvas, rectF: RectF, paint: Paint) {\n        canvas.withRotation(initialRotation * 90.0f, rectF.centerX(), rectF.centerY()) {\n            val vectorDrawable = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {\n                runCatching { srcImgSvg.constantState!!.newDrawable().mutate() }\n                    .getOrDefault(srcImgSvg)\n            } else srcImgSvg\n            val rect = Rect()\n            rectF.round(rect)\n            vectorDrawable.bounds = rect\n            vectorDrawable.colorFilter = paint.colorFilter\n            vectorDrawable.draw(this)\n        }\n    }\n\n    private fun drawForNone(canvas: Canvas, j: Long) {\n        draw(canvas, destRect, paint)\n    }\n\n    private fun drawForZoomIn(canvas: Canvas, j: Long) {\n        val j2 = duration\n        val f = if (j2 > 0) j2.toFloat() else 1000f\n        val f2 = j.toFloat()\n        if (f2 / f < skipStartProgress) {\n            return\n        }\n        if (f2 <= f) {\n            val cos = (cos(((f2 - 1000.0f) / 1000.0f) * 3.1415927f) + 1.0f) / 2.0f\n            draw(\n                canvas,\n                RectF(\n                    destRect.centerX() - ((destRect.width() / 2.0f) * cos),\n                    destRect.centerY() - ((destRect.height() / 2.0f) * cos),\n                    destRect.centerX() + ((destRect.width() / 2.0f) * cos),\n                    destRect.centerY() + ((destRect.height() / 2.0f) * cos)\n                ),\n                paint\n            )\n        } else {\n            drawForNone(canvas, j)\n        }\n    }\n\n    private fun drawForEmphasizedZoomIn(canvas: Canvas, j: Long) {\n        val j2 = duration\n        val f = if (j2 > 0) j2.toFloat() else 1000f\n        val f2 = j.toFloat()\n        if (f2 <= f) {\n            val interpolation = EmphasizedInterpolator.getInterpolation(f2 / f)\n            draw(\n                canvas,\n                RectF(\n                    destRect.centerX() - ((destRect.width() / 2.0f) * interpolation),\n                    destRect.centerY() - ((destRect.height() / 2.0f) * interpolation),\n                    destRect.centerX() + ((destRect.width() / 2.0f) * interpolation),\n                    destRect.centerY() + ((destRect.height() / 2.0f) * interpolation)\n                ), paint\n            )\n        } else {\n            drawForNone(canvas, j)\n        }\n    }\n\n    private fun drawForSpringZoomIn(canvas: Canvas, j: Long) {\n        if (j <= 1500) {\n            val springScale = calculateSpringScale(j)\n            draw(\n                canvas,\n                RectF(\n                    destRect.centerX() - ((destRect.width() / 2.0f) * springScale),\n                    destRect.centerY() - ((destRect.height() / 2.0f) * springScale),\n                    destRect.centerX() + ((destRect.width() / 2.0f) * springScale),\n                    destRect.centerY() + ((destRect.height() / 2.0f) * springScale)\n                ), paint\n            )\n        } else {\n            drawForNone(canvas, j)\n        }\n    }\n\n    private fun drawForRotateEmphasizedZoomIn(canvas: Canvas, j: Long) {\n        canvas.withSave {\n            val j2 = duration\n            val f = if (j2 > 0) j2.toFloat() else 1000f\n            val f2 = j.toFloat()\n            var f3 = ((f2 - f) * 360.0f) / 4410.0f\n            if (f2 <= f) {\n                val interpolation = EmphasizedInterpolator.getInterpolation(f2 / f)\n                val rectF = RectF(\n                    destRect.centerX() - ((destRect.width() / 2.0f) * interpolation),\n                    destRect.centerY() - ((destRect.height() / 2.0f) * interpolation),\n                    destRect.centerX() + ((destRect.width() / 2.0f) * interpolation),\n                    destRect.centerY() + ((destRect.height() / 2.0f) * interpolation)\n                )\n                rotate(\n                    (interpolation * 180.0f) + f3,\n                    destRect.centerX(),\n                    destRect.centerY()\n                )\n                draw(this, rectF, paint)\n            } else {\n                if (isMotionPaused) {\n                    f3 = 0.0f\n                }\n                rotate(f3, destRect.centerX(), destRect.centerY())\n                draw(this, destRect, paint)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/shared/src/main/proto/blacklist.proto",
    "content": "syntax = \"proto3\";\n\npackage dev.aaa1115910.bv;\n\nmessage BlacklistNano {\n  int32 version = 1;\n  int32 count = 2;\n  repeated int64 uids = 3;\n}"
  },
  {
    "path": "app/shared/src/main/res/drawable/ic_banner_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"320dp\"\n    android:height=\"180dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <group android:scaleX=\"0.6666667\"\n      android:scaleY=\"0.6666667\"\n      android:translateX=\"170.66667\"\n      android:translateY=\"170.66667\">\n    <group android:scaleX=\"0.8\"\n        android:scaleY=\"0.8\"\n        android:translateX=\"102.4\"\n        android:translateY=\"102.4\">\n        <group\n            android:scaleX=\"1.125\"\n            android:scaleY=\"2\"\n            android:translateX=\"-64\"\n            android:translateY=\"-512\">\n            <path\n                android:fillColor=\"#fb7299\"\n                android:pathData=\"M704.1,703.8h-384c-35.3,0 -64,-28.7 -64,-64v-192c0,-35.3 28.7,-64 64,-64h384c35.3,0 64,28.7 64,64v192C768.1,675.1 739.5,703.8 704.1,703.8zM832.1,448.2h-64v64h64V448.2zM832.1,576h-64v64h64V576zM256.1,448.2h-64v64h64V448.2zM256.1,576h-64v64h64V576zM666.6,293.3l-90.5,90.5h90.5l45.3,-45.3L666.6,293.3zM448.1,383.8l-90.5,-90.5l-45.3,45.3l45.3,45.3H448.1zM480.1,479.5h-64v128h64V479.5zM608.1,479.8h-64v128h64V479.8z\" />\n        </group>\n    </group>\n  </group>\n</vector>\n"
  },
  {
    "path": "app/shared/src/main/res/drawable/ic_banner_foreground_2.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"320dp\"\n    android:height=\"180dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <group android:scaleX=\"0.6666667\"\n      android:scaleY=\"0.6666667\"\n      android:translateX=\"170.66667\"\n      android:translateY=\"170.66667\">\n    <group android:scaleX=\"1.3\"\n        android:scaleY=\"1.3\"\n        android:translateX=\"-153.6\"\n        android:translateY=\"-153.6\">\n        <group\n            android:scaleX=\"1.125\"\n            android:scaleY=\"2\"\n            android:translateX=\"-64\"\n            android:translateY=\"-512\">\n            <path\n                android:fillColor=\"#fb7299\"\n                android:pathData=\"M704.1,703.8h-384c-35.3,0 -64,-28.7 -64,-64v-192c0,-35.3 28.7,-64 64,-64h384c35.3,0 64,28.7 64,64v192C768.1,675.1 739.5,703.8 704.1,703.8zM832.1,448.2h-64v64h64V448.2zM832.1,576h-64v64h64V576zM256.1,448.2h-64v64h64V448.2zM256.1,576h-64v64h64V576zM666.6,293.3l-90.5,90.5h90.5l45.3,-45.3L666.6,293.3zM448.1,383.8l-90.5,-90.5l-45.3,45.3l45.3,45.3H448.1zM480.1,479.5h-64v128h64V479.5zM608.1,479.8h-64v128h64V479.8z\" />\n        </group>\n    </group>\n  </group>\n</vector>\n"
  },
  {
    "path": "app/shared/src/main/res/drawable/ic_danmaku_count.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:viewportWidth=\"16\"\n    android:viewportHeight=\"16\">\n  <path\n      android:pathData=\"M8,3.25c-1.714,0 -3.208,0.088 -4.258,0.174A1.45,1.45 0,0 0,2.4 4.746C2.323,5.607 2.25,6.75 2.25,8s0.073,2.393 0.15,3.254a1.45,1.45 0,0 0,1.342 1.322c1.05,0.086 2.544,0.174 4.258,0.174s3.208,-0.088 4.258,-0.174a1.45,1.45 0,0 0,1.341 -1.321c0.078,-0.862 0.151,-2.004 0.151,-3.255s-0.073,-2.393 -0.15,-3.255a1.45,1.45 0,0 0,-1.342 -1.321A52.956,52.956 0,0 0,8 3.25ZM3.66,2.427A53.955,53.955 0,0 1,8 2.25c1.747,0 3.27,0.09 4.34,0.177a2.45,2.45 0,0 1,2.255 2.228c0.08,0.883 0.155,2.057 0.155,3.345 0,1.288 -0.075,2.462 -0.155,3.345a2.45,2.45 0,0 1,-2.255 2.228c-1.07,0.087 -2.593,0.177 -4.34,0.177 -1.747,0 -3.27,-0.09 -4.34,-0.177a2.45,2.45 0,0 1,-2.255 -2.229A37.662,37.662 0,0 1,1.25 8c0,-1.288 0.075,-2.461 0.155,-3.344A2.45,2.45 0,0 1,3.66 2.427ZM4,6.75a0.5,0.5 0,0 1,0.5 -0.5h0.25a0.5,0.5 0,0 1,0 1L4.5,7.25a0.5,0.5 0,0 1,-0.5 -0.5ZM6,6.75a0.5,0.5 0,0 1,0.5 -0.5h4a0.5,0.5 0,0 1,0 1h-4a0.5,0.5 0,0 1,-0.5 -0.5ZM5.5,8.75a0.5,0.5 0,0 0,0 1h0.25a0.5,0.5 0,0 0,0 -1L5.5,8.75ZM7.5,8.75a0.5,0.5 0,0 0,0 1h4a0.5,0.5 0,0 0,0 -1h-4Z\"\n      android:fillColor=\"#FFFFFF\"\n      android:fillType=\"evenOdd\"/>\n</vector>\n"
  },
  {
    "path": "app/shared/src/main/res/drawable/ic_gamer_ani.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"64.0\"\n    android:viewportHeight=\"72.0\">\n    <group>\n        <clip-path android:pathData=\"M0,0h63.729v72h-63.729z\" />\n        <path\n            android:fillColor=\"#FFDEF3FF\"\n            android:pathData=\"M49.168,68.519C50.142,70.861 41.682,71.999 32.448,71.999C23.213,71.999 14.851,70.438 15.727,68.519C16.451,66.935 23.213,65.689 32.448,65.689C41.682,65.689 48.499,66.91 49.168,68.519Z\" />\n        <path\n            android:fillColor=\"#FFDEF3FF\"\n            android:pathData=\"M13.166,62.048C12.441,62.048 11.712,61.862 10.801,61.447L5.918,59.231C4.709,58.7 3.84,57.56 3.654,56.253L0.038,31.134C-0.172,29.658 0.494,28.179 1.737,27.362L7.094,23.713C7.117,23.697 7.142,23.681 7.166,23.666L7.215,23.637C7.646,23.379 8.539,22.94 9.629,22.94C10.718,22.94 11.691,23.362 12.398,24.131C13.059,24.852 13.405,25.812 13.377,26.843C13.526,28.977 16.411,55.31 16.686,57.047C16.819,57.639 17.159,59.28 16.032,60.693C15.347,61.553 14.301,62.047 13.165,62.047L13.166,62.048ZM12.076,57.005C11.663,53.708 10.558,43.689 10.415,42.381C9.431,33.443 9.044,29.66 8.894,28.022L4.631,30.925L8.125,55.21L12.077,57.005H12.076Z\" />\n        <path\n            android:fillColor=\"#FFDEF3FF\"\n            android:pathData=\"M51.384,15.878L51.504,15.81C53.425,14.761 53.686,13.023 53.093,11.731C52.891,11.186 51.836,8.506 49.692,5.754C47.764,3.282 45.794,1.473 45.227,0.988C44.562,0.35 43.698,0 42.795,0C42.17,0 41.552,0.172 41.011,0.496C40.862,0.585 40.72,0.691 40.584,0.816C39.214,2.088 32.725,8.227 30.305,12.421C30.094,12.405 29.884,12.398 29.678,12.398C29.365,12.398 29.046,12.417 28.718,12.455C26.595,8.696 21.579,4.292 20.97,3.764C20.935,3.733 20.898,3.704 20.86,3.677C20.328,3.299 19.705,3.1 19.056,3.1C18.879,3.1 18.702,3.116 18.53,3.145C17.829,3.264 17.437,3.563 17.023,3.879L16.933,3.948C16.915,3.961 16.899,3.975 16.882,3.989C16.17,4.582 14.368,6.165 13.03,8.033C11.466,10.221 10.992,12.715 10.861,13.686C10.602,15.073 11.027,16.409 12.123,17.077C12.323,17.199 16.535,18.878 19.059,19.476H20.641L26.234,19.377C26.416,18.994 26.883,18.154 27.859,17.59H27.86C28.125,17.436 28.743,17.088 29.618,17.074C29.749,17.072 30.427,17.068 31.198,17.429C32.276,17.933 32.831,18.821 33.055,19.256L40.428,19.124C40.508,19.122 40.587,19.114 40.665,19.1L41.183,19.003C45.412,17.614 51.286,15.93 51.339,15.903C51.354,15.895 51.369,15.887 51.384,15.878ZM22.099,16.359C21.923,16.232 21.728,16.128 21.515,16.046C21.225,15.936 20.916,15.804 20.544,15.642C20.544,15.642 20.537,15.639 20.534,15.638C19.222,15.081 17.643,14.314 16.143,13.508C16.366,12.734 16.736,11.664 17.247,10.891C17.72,10.176 18.42,9.447 19.029,8.875C20.783,10.529 22.88,12.736 23.975,14.408C23.245,14.965 22.615,15.624 22.1,16.359H22.099ZM36.705,15.685C36.247,15.145 35.725,14.659 35.145,14.234C36.69,11.952 39.989,8.463 42.757,5.776C42.765,5.768 42.785,5.748 42.824,5.748C42.854,5.748 42.882,5.76 42.905,5.784C43.599,6.469 44.644,7.565 45.674,8.887C46.558,10.02 47.213,11.152 47.656,12.031C41.477,14.929 38.064,15.556 36.705,15.685Z\" />\n        <path\n            android:fillColor=\"#FFDEF3FF\"\n            android:pathData=\"M48.343,39.169C47.285,39.169 46.425,38.308 46.425,37.25V33.21C46.425,32.152 47.285,31.291 48.343,31.291H48.835C49.891,31.291 50.751,32.152 50.753,33.209V37.25C50.753,38.308 49.893,39.169 48.835,39.169H48.343Z\" />\n        <path\n            android:fillColor=\"#FFDEF3FF\"\n            android:pathData=\"M32.771,37.84C32.771,39.01 33.722,39.961 34.889,39.961H35.304C36.47,39.961 37.421,39.009 37.421,37.841V33.725C37.421,32.557 36.471,31.607 35.304,31.607H34.889C33.721,31.607 32.771,32.559 32.771,33.727V37.84Z\" />\n        <path\n            android:fillColor=\"#FFDEF3FF\"\n            android:pathData=\"M42.843,49.449C43.159,49.633 43.489,49.725 43.825,49.725V49.726C44.287,49.726 44.729,49.548 45.106,49.211C46.189,48.239 47.5,47.137 49.007,46.352C49.38,46.16 49.654,45.832 49.781,45.433C49.908,45.033 49.87,44.608 49.678,44.239C49.408,43.714 48.874,43.388 48.285,43.388C48.035,43.388 47.784,43.449 47.563,43.565C45.965,44.395 44.65,45.443 43.653,46.305C42.124,45.496 40.398,44.823 38.243,44.194C38.099,44.152 37.949,44.131 37.801,44.131C37.109,44.131 36.492,44.595 36.298,45.26C36.18,45.663 36.226,46.087 36.427,46.454C36.629,46.822 36.961,47.089 37.364,47.206C39.625,47.868 41.366,48.58 42.843,49.449Z\" />\n        <path\n            android:fillColor=\"#FFDEF3FF\"\n            android:fillType=\"evenOdd\"\n            android:pathData=\"M16.841,21.796C17.023,21.399 17.263,21.023 17.576,20.717C17.982,20.323 18.485,20.116 19.027,19.987C19.35,19.91 19.674,19.841 20,19.778L42.308,19.296L59.286,19.221C61.582,19.221 63.451,21.078 63.458,23.364L63.728,54.381C63.728,56.532 62.063,58.347 59.927,58.538L24.772,63.521C24.611,63.545 24.437,63.558 24.265,63.558C22.065,63.558 20.243,61.847 20.102,59.655L16.435,23.966C16.325,23.25 16.543,22.447 16.841,21.796ZM58.628,53.582L58.379,24.316L21.61,24.51L25.086,58.336L58.628,53.582Z\" />\n    </group>\n</vector>\n"
  },
  {
    "path": "app/shared/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n    <group\n        android:scaleX=\"0.72794116\"\n        android:scaleY=\"0.72794116\"\n        android:translateX=\"139.29411\"\n        android:translateY=\"139.29411\">\n        <path\n            android:fillColor=\"#fb7299\"\n            android:pathData=\"M704.1,703.8h-384c-35.3,0 -64,-28.7 -64,-64v-192c0,-35.3 28.7,-64 64,-64h384c35.3,0 64,28.7 64,64v192C768.1,675.1 739.5,703.8 704.1,703.8zM832.1,448.2h-64v64h64V448.2zM832.1,576h-64v64h64V576zM256.1,448.2h-64v64h64V448.2zM256.1,576h-64v64h64V576zM666.6,293.3l-90.5,90.5h90.5l45.3,-45.3L666.6,293.3zM448.1,383.8l-90.5,-90.5l-45.3,45.3l45.3,45.3H448.1zM480.1,479.5h-64v128h64V479.5zM608.1,479.8h-64v128h64V479.8z\" />\n    </group>\n</vector>\n"
  },
  {
    "path": "app/shared/src/main/res/drawable/ic_launcher_foreground_2.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n    <group\n        android:scaleX=\"1.2\"\n        android:scaleY=\"1.2\"\n        android:translateX=\"-102.4\"\n        android:translateY=\"-102.4\">\n        <path\n            android:fillColor=\"#fb7299\"\n            android:pathData=\"M704.1,703.8h-384c-35.3,0 -64,-28.7 -64,-64v-192c0,-35.3 28.7,-64 64,-64h384c35.3,0 64,28.7 64,64v192C768.1,675.1 739.5,703.8 704.1,703.8zM832.1,448.2h-64v64h64V448.2zM832.1,576h-64v64h64V576zM256.1,448.2h-64v64h64V448.2zM256.1,576h-64v64h64V576zM666.6,293.3l-90.5,90.5h90.5l45.3,-45.3L666.6,293.3zM448.1,383.8l-90.5,-90.5l-45.3,45.3l45.3,45.3H448.1zM480.1,479.5h-64v128h64V479.5zM608.1,479.8h-64v128h64V479.8z\" />\n    </group>\n</vector>\n"
  },
  {
    "path": "app/shared/src/main/res/drawable/ic_play_count.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:viewportWidth=\"16\"\n    android:viewportHeight=\"16\">\n  <path\n      android:pathData=\"M3.742,3.424A52.952,52.952 0,0 1,8 3.25c1.714,0 3.208,0.088 4.258,0.174A1.45,1.45 0,0 1,13.6 4.745c0.078,0.862 0.151,2.004 0.151,3.255s-0.073,2.393 -0.15,3.255a1.45,1.45 0,0 1,-1.342 1.321c-1.05,0.086 -2.544,0.174 -4.258,0.174s-3.208,-0.088 -4.258,-0.174A1.45,1.45 0,0 1,2.4 11.254,36.666 36.666,0 0,1 2.25,8c0,-1.25 0.073,-2.393 0.15,-3.254a1.45,1.45 0,0 1,1.342 -1.322ZM8,2.25c-1.747,0 -3.27,0.09 -4.34,0.177a2.45,2.45 0,0 0,-2.255 2.229C1.325,5.539 1.25,6.712 1.25,8c0,1.288 0.075,2.461 0.155,3.344a2.45,2.45 0,0 0,2.255 2.229A53.91,53.91 0,0 0,8 13.75c1.747,0 3.27,-0.09 4.34,-0.177a2.45,2.45 0,0 0,2.255 -2.229c0.08,-0.882 0.155,-2.056 0.155,-3.344 0,-1.288 -0.075,-2.462 -0.155,-3.345a2.45,2.45 0,0 0,-2.255 -2.228A53.953,53.953 0,0 0,8 2.25ZM9.75,8.578a0.667,0.667 0,0 0,0 -1.155l-2.5,-1.444a0.667,0.667 0,0 0,-1 0.577v2.888c0,0.513 0.555,0.834 1,0.578l2.5,-1.444Z\"\n      android:fillColor=\"#FFFFFF\"\n      android:fillType=\"evenOdd\"/>\n</vector>\n"
  },
  {
    "path": "app/shared/src/main/res/drawable/ic_up.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:pathData=\"M6.15,8.248C6.564,8.248 6.9,8.584 6.9,8.998L6.9,12.774C6.9,13.588 7.56,14.248 8.374,14.248C9.188,14.248 9.848,13.588 9.848,12.774L9.848,8.998C9.848,8.584 10.184,8.248 10.598,8.248C11.012,8.248 11.348,8.584 11.348,8.998L11.348,12.774C11.348,14.417 10.016,15.748 8.374,15.748C6.731,15.748 5.4,14.417 5.4,12.774L5.4,8.998C5.4,8.584 5.736,8.248 6.15,8.248z\"\n        android:fillColor=\"#FFFFFF\" />\n    <path\n        android:pathData=\"M12.652,8.998C12.652,8.584 12.988,8.248 13.402,8.248L15.725,8.248C17.313,8.248 18.6,9.535 18.6,11.123C18.6,12.711 17.313,13.998 15.725,13.998L14.152,13.998L14.152,14.998C14.152,15.412 13.816,15.748 13.402,15.748C12.988,15.748 12.652,15.412 12.652,14.998L12.652,8.998zM14.152,12.498L15.725,12.498C16.484,12.498 17.1,11.882 17.1,11.123C17.1,10.364 16.484,9.748 15.725,9.748L14.152,9.748L14.152,12.498z\"\n        android:fillColor=\"#FFFFFF\" />\n    <path\n        android:pathData=\"M12,4.998C9.482,4.998 7.283,5.126 5.731,5.252C4.652,5.339 3.816,6.164 3.72,7.233C3.606,8.5 3.5,10.171 3.5,11.998C3.5,13.825 3.606,15.496 3.72,16.764C3.816,17.833 4.652,18.657 5.731,18.744C7.283,18.87 9.482,18.998 12,18.998C14.519,18.998 16.717,18.87 18.27,18.744C19.348,18.657 20.184,17.833 20.28,16.764C20.394,15.497 20.5,13.826 20.5,11.998C20.5,10.17 20.394,8.499 20.28,7.232C20.184,6.163 19.348,5.34 18.27,5.252C16.717,5.126 14.519,4.998 12,4.998zM5.61,3.757C7.192,3.629 9.433,3.498 12,3.498C14.568,3.498 16.808,3.629 18.391,3.757C20.188,3.903 21.612,5.293 21.774,7.098C21.891,8.397 22,10.114 22,11.998C22,13.882 21.891,15.599 21.774,16.898C21.612,18.703 20.188,20.093 18.391,20.239C16.808,20.368 14.568,20.498 12,20.498C9.433,20.498 7.192,20.368 5.61,20.239C3.812,20.093 2.388,18.703 2.226,16.898C2.109,15.598 2,13.881 2,11.998C2,10.115 2.109,8.398 2.226,7.098C2.388,5.293 3.812,3.903 5.61,3.757z\"\n        android:fillColor=\"#FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_hor_bar_s2_capsule.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"17.0dip\"\n    android:width=\"34.0dip\"\n    android:viewportWidth=\"34.0\"\n    android:viewportHeight=\"17.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M25.5,15L8.5,15A6.5,6.5 0,0 1,2 8.5L2,8.5A6.5,6.5 0,0 1,8.5 2L25.5,2A6.5,6.5 0,0 1,32 8.5L32,8.5A6.5,6.5 0,0 1,25.5 15z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_hor_bar_s2_half_capsule.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"17.0dip\"\n    android:width=\"34.0dip\"\n    android:viewportWidth=\"34.0\"\n    android:viewportHeight=\"17.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M32,8.5C32,12.09 29.09,15 25.5,15L2,15L2,2L25.5,2C29.09,2 32,4.91 32,8.5V8.5Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_hor_bar_s3_capsule.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"17.0dip\"\n    android:width=\"51.0dip\"\n    android:viewportWidth=\"51.0\"\n    android:viewportHeight=\"17.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M42.5,15L8.5,15A6.5,6.5 0,0 1,2 8.5L2,8.5A6.5,6.5 0,0 1,8.5 2L42.5,2A6.5,6.5 0,0 1,49 8.5L49,8.5A6.5,6.5 0,0 1,42.5 15z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_hor_bar_s3_half_capsule.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"17.0dip\"\n    android:width=\"51.0dip\"\n    android:viewportWidth=\"51.0\"\n    android:viewportHeight=\"17.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M49,8.5C49,12.09 46.09,15 42.5,15L2,15L2,2L42.5,2C46.09,2 49,4.91 49,8.5V8.5Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s1_circle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"17.0dip\"\n    android:width=\"17.0dip\"\n    android:viewportWidth=\"17.0\"\n    android:viewportHeight=\"17.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M8.5,8.5m-6.5,0a6.5,6.5 0,1 1,13 0a6.5,6.5 0,1 1,-13 0\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s1_drop.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"17.0dip\"\n    android:width=\"17.0dip\"\n    android:viewportWidth=\"17.0\"\n    android:viewportHeight=\"17.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M2,8.5C2,4.91 4.91,2 8.5,2V2C12.09,2 15,4.91 15,8.5V15H8.5C4.91,15 2,12.09 2,8.5V8.5Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s1_semi_circle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"17.0dip\"\n    android:width=\"17.0dip\"\n    android:viewportWidth=\"17.0\"\n    android:viewportHeight=\"17.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M2,8.5C2,4.91 4.91,2 8.5,2H15V15H8.5C4.91,15 2,12.09 2,8.5V8.5Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s1_square.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"17.0dip\"\n    android:width=\"17.0dip\"\n    android:viewportWidth=\"17.0\"\n    android:viewportHeight=\"17.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M5,2L12,2A3,3 0,0 1,15 5L15,12A3,3 0,0 1,12 15L5,15A3,3 0,0 1,2 12L2,5A3,3 0,0 1,5 2z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s2_circle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"34.0dip\"\n    android:width=\"34.0dip\"\n    android:viewportWidth=\"34.0\"\n    android:viewportHeight=\"34.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M17,17m-15,0a15,15 0,1 1,30 0a15,15 0,1 1,-30 0\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s2_clover.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"34.0dip\"\n    android:width=\"34.0dip\"\n    android:viewportWidth=\"34.0\"\n    android:viewportHeight=\"34.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M2.665,12.851C-0.134,6.407 6.407,-0.134 12.851,2.665L13.917,3.128C15.883,3.982 18.117,3.982 20.085,3.128L21.149,2.665C27.594,-0.134 34.133,6.407 31.335,12.851L30.872,13.916C30.019,15.883 30.019,18.117 30.872,20.084L31.335,21.15C34.133,27.593 27.594,34.134 21.149,31.335L20.085,30.872C18.117,30.018 15.883,30.018 13.917,30.872L12.851,31.335C6.407,34.134 -0.134,27.593 2.665,21.15L3.128,20.084C3.983,18.117 3.983,15.883 3.128,13.916L2.665,12.851Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s2_hexagonal.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"34.0dip\"\n    android:width=\"34.0dip\"\n    android:viewportWidth=\"34.0\"\n    android:viewportHeight=\"34.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M14.372,2.821C15.831,1.726 17.861,1.726 19.32,2.821L22.988,5.572C23.271,5.783 23.58,5.958 23.909,6.091L28.183,7.818C29.883,8.505 30.898,10.222 30.657,12.003L30.052,16.481C30.005,16.826 30.005,17.175 30.052,17.52L30.657,21.998C30.898,23.778 29.883,25.496 28.183,26.184L23.909,27.909C23.58,28.043 23.271,28.217 22.988,28.428L19.32,31.179C17.861,32.274 15.831,32.274 14.372,31.179L10.704,28.428C10.422,28.217 10.112,28.043 9.783,27.909L5.51,26.184C3.809,25.496 2.794,23.778 3.035,21.998L3.641,17.52C3.687,17.175 3.687,16.826 3.641,16.481L3.035,12.003C2.794,10.222 3.809,8.505 5.51,7.818L9.783,6.091C10.112,5.958 10.422,5.783 10.704,5.572L14.372,2.821Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s2_meteroid.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"34.0dip\"\n    android:width=\"34.0dip\"\n    android:viewportWidth=\"34.0\"\n    android:viewportHeight=\"34.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M15.355,2.443C16.471,1.852 17.799,1.852 18.914,2.443L20.781,3.433C21.156,3.632 21.562,3.766 21.98,3.831L24.059,4.154C25.302,4.346 26.375,5.144 26.938,6.293L27.881,8.216C28.069,8.603 28.321,8.956 28.622,9.26L30.118,10.771C31.013,11.674 31.422,12.965 31.217,14.233L30.876,16.355C30.806,16.782 30.806,17.218 30.876,17.645L31.217,19.767C31.422,21.035 31.013,22.326 30.118,23.229L28.622,24.74C28.321,25.045 28.069,25.396 27.881,25.783L26.938,27.706C26.375,28.855 25.302,29.653 24.059,29.846L21.98,30.169C21.562,30.234 21.156,30.369 20.781,30.567L18.914,31.556C17.799,32.148 16.471,32.148 15.355,31.556L13.489,30.567C13.113,30.369 12.707,30.234 12.289,30.169L10.21,29.846C8.967,29.653 7.894,28.855 7.331,27.706L6.388,25.783C6.2,25.396 5.948,25.045 5.647,24.74L4.151,23.229C3.256,22.326 2.847,21.035 3.052,19.767L3.394,17.645C3.463,17.218 3.463,16.782 3.394,16.355L3.052,14.233C2.847,12.965 3.256,11.674 4.151,10.771L5.647,9.26C5.948,8.956 6.2,8.603 6.388,8.216L7.331,6.293C7.894,5.144 8.967,4.346 10.21,4.154L12.289,3.831C12.707,3.766 13.113,3.632 13.489,3.433L15.355,2.443Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s2_wiggle_star.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"34.0dip\"\n    android:width=\"34.0dip\"\n    android:viewportWidth=\"34.0\"\n    android:viewportHeight=\"34.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M2.818,19.181C1.727,17.931 1.727,16.068 2.818,14.819L4.451,12.947C4.927,12.402 5.212,11.715 5.261,10.992L5.43,8.515C5.542,6.86 6.859,5.543 8.514,5.43L10.992,5.261C11.715,5.213 12.402,4.928 12.948,4.452L14.819,2.818C16.069,1.727 17.931,1.727 19.181,2.818L21.053,4.452C21.598,4.928 22.285,5.213 23.008,5.261L25.485,5.43C27.14,5.543 28.457,6.86 28.57,8.515L28.739,10.992C28.787,11.715 29.072,12.402 29.548,12.947L31.182,14.819C32.273,16.068 32.273,17.931 31.182,19.181L29.548,21.052C29.072,21.598 28.787,22.285 28.739,23.008L28.57,25.486C28.457,27.141 27.14,28.458 25.485,28.57L23.008,28.739C22.285,28.788 21.598,29.073 21.053,29.549L19.181,31.182C17.931,32.273 16.069,32.273 14.819,31.182L12.948,29.549C12.402,29.073 11.715,28.788 10.992,28.739L8.514,28.57C6.859,28.458 5.542,27.141 5.43,25.486L5.261,23.008C5.212,22.285 4.927,21.598 4.451,21.052L2.818,19.181Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s3_circle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"51.0dip\"\n    android:width=\"51.0dip\"\n    android:viewportWidth=\"51.0\"\n    android:viewportHeight=\"51.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M25.5,25.5m-23.5,0a23.5,23.5 0,1 1,47 0a23.5,23.5 0,1 1,-47 0\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s3_clover.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"51.0dip\"\n    android:width=\"51.0dip\"\n    android:viewportWidth=\"51.0\"\n    android:viewportHeight=\"51.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M3.042,18.999C-1.343,8.904 8.904,-1.343 18.999,3.042L20.67,3.767C23.75,5.106 27.25,5.106 30.333,3.767L32,3.042C42.098,-1.343 52.342,8.904 47.958,18.999L47.233,20.668C45.896,23.75 45.896,27.25 47.233,30.332L47.958,32.001C52.342,42.096 42.098,52.343 32,47.958L30.333,47.233C27.25,45.895 23.75,45.895 20.67,47.233L18.999,47.958C8.904,52.343 -1.343,42.096 3.042,32.001L3.767,30.332C5.106,27.25 5.106,23.75 3.767,20.668L3.042,18.999Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s3_hexagonal.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"51.0dip\"\n    android:width=\"51.0dip\"\n    android:viewportWidth=\"51.0\"\n    android:viewportHeight=\"51.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M21.816,3.286C24.102,1.571 27.283,1.571 29.569,3.286L35.315,7.596C35.757,7.927 36.243,8.201 36.758,8.409L43.453,11.115C46.117,12.191 47.707,14.881 47.329,17.672L46.381,24.687C46.308,25.227 46.308,25.774 46.381,26.314L47.329,33.33C47.707,36.119 46.117,38.81 43.453,39.888L36.758,42.591C36.243,42.801 35.757,43.073 35.315,43.404L29.569,47.714C27.283,49.429 24.102,49.429 21.816,47.714L16.07,43.404C15.627,43.073 15.142,42.801 14.627,42.591L7.932,39.888C5.268,38.81 3.678,36.119 4.055,33.33L5.004,26.314C5.077,25.774 5.077,25.227 5.004,24.687L4.055,17.672C3.678,14.881 5.268,12.191 7.932,11.115L14.627,8.409C15.142,8.201 15.627,7.927 16.07,7.596L21.816,3.286Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s3_meteroid.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"51.0dip\"\n    android:width=\"51.0dip\"\n    android:viewportWidth=\"51.0\"\n    android:viewportHeight=\"51.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M22.356,2.695C24.104,1.768 26.185,1.768 27.932,2.695L30.856,4.244C31.445,4.557 32.081,4.767 32.735,4.869L35.993,5.374C37.94,5.676 39.621,6.926 40.503,8.726L41.98,11.739C42.276,12.345 42.669,12.898 43.142,13.374L45.485,15.741C46.887,17.155 47.528,19.178 47.207,21.164L46.672,24.489C46.563,25.158 46.563,25.841 46.672,26.51L47.207,29.835C47.528,31.821 46.887,33.844 45.485,35.259L43.142,37.625C42.669,38.103 42.276,38.654 41.98,39.261L40.503,42.273C39.621,44.073 37.94,45.323 35.993,45.626L32.735,46.132C32.081,46.233 31.445,46.445 30.856,46.755L27.932,48.305C26.185,49.232 24.104,49.232 22.356,48.305L19.432,46.755C18.844,46.445 18.207,46.233 17.553,46.132L14.296,45.626C12.349,45.323 10.667,44.073 9.786,42.273L8.308,39.261C8.013,38.654 7.619,38.103 7.147,37.625L4.803,35.259C3.402,33.844 2.76,31.821 3.081,29.835L3.616,26.51C3.725,25.841 3.725,25.158 3.616,24.489L3.081,21.164C2.76,19.178 3.402,17.155 4.803,15.741L7.147,13.374C7.619,12.898 8.013,12.345 8.308,11.739L9.786,8.726C10.667,6.926 12.349,5.676 14.296,5.374L17.553,4.869C18.207,4.767 18.844,4.557 19.432,4.244L22.356,2.695Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s3_wiggle_star.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"51.0dip\"\n    android:width=\"51.0dip\"\n    android:viewportWidth=\"51.0\"\n    android:viewportHeight=\"51.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M3.281,28.917C1.573,26.959 1.573,24.04 3.281,22.084L5.84,19.151C6.586,18.296 7.032,17.22 7.109,16.088L7.373,12.206C7.549,9.613 9.613,7.551 12.205,7.374L16.087,7.109C17.22,7.034 18.296,6.587 19.152,5.841L22.083,3.282C24.041,1.573 26.959,1.573 28.917,3.282L31.849,5.841C32.704,6.587 33.78,7.034 34.912,7.109L38.794,7.374C41.387,7.551 43.449,9.613 43.626,12.206L43.891,16.088C43.966,17.22 44.413,18.296 45.159,19.151L47.718,22.084C49.427,24.04 49.427,26.959 47.718,28.917L45.159,31.848C44.413,32.704 43.966,33.78 43.891,34.913L43.626,38.795C43.449,41.387 41.387,43.451 38.794,43.627L34.912,43.891C33.78,43.968 32.704,44.414 31.849,45.16L28.917,47.719C26.959,49.427 24.041,49.427 22.083,47.719L19.152,45.16C18.296,44.414 17.22,43.968 16.087,43.891L12.205,43.627C9.613,43.451 7.549,41.387 7.373,38.795L7.109,34.913C7.032,33.78 6.586,32.704 5.84,31.848L3.281,28.917Z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_square_s7_ring.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"119.0dip\"\n    android:width=\"119.0dip\"\n    android:viewportWidth=\"119.0\"\n    android:viewportHeight=\"119.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59.5,59.5m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0\"\n        android:strokeColor=\"#ffa3d69d\"\n        android:strokeWidth=\"15.0\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_ver_bar_s2_capsule.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"34.0dip\"\n    android:width=\"17.0dip\"\n    android:viewportWidth=\"17.0\"\n    android:viewportHeight=\"34.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M15,8.5L15,25.5A6.5,6.5 0,0 1,8.5 32L8.5,32A6.5,6.5 0,0 1,2 25.5L2,8.5A6.5,6.5 0,0 1,8.5 2L8.5,2A6.5,6.5 0,0 1,15 8.5z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/drawable/qrcode_ver_bar_s3_capsule.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"51.0dip\"\n    android:width=\"17.0dip\"\n    android:viewportWidth=\"17.0\"\n    android:viewportHeight=\"51.0\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#ff5f905f\"\n        android:pathData=\"M15,8.5L15,42.5A6.5,6.5 0,0 1,8.5 49L8.5,49A6.5,6.5 0,0 1,2 42.5L2,8.5A6.5,6.5 0,0 1,8.5 2L8.5,2A6.5,6.5 0,0 1,15 8.5z\" />\n</vector>"
  },
  {
    "path": "app/shared/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/shared/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/shared/src/main/res/raw/ic_playing.json",
    "content": "{\"v\":\"5.7.1\",\"fr\":60,\"ip\":0,\"op\":55,\"w\":200,\"h\":200,\"nm\":\"Frame 38\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":4,\"nm\":\"Third\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"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\":22,\"s\":[147,100.43,0],\"to\":[0,-3.333,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":37,\"s\":[147,80.43,0],\"to\":[0,0,0],\"ti\":[0,-3.333,0]},{\"t\":53,\"s\":[147,100.43,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[10.968,36.17],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":20,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0.933333333333,0.78431372549,0.270588235294,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":5,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.933333393172,0.784313785329,0.270588235294,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\":[-24.117,25.326],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.141,19.847],\"ix\":1},\"s\":{\"a\":0,\"k\":[71.471,84.703],\"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\":\"Rectangle 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":80,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"bg-third\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[147,100,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[10.968,36.17],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":20,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0.933333333333,0.78431372549,0.270588235294,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":5,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.933333393172,0.784313785329,0.270588235294,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\":[-24.117,25.326],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.141,19.847],\"ix\":1},\"s\":{\"a\":0,\"k\":[71.471,84.703],\"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\":\"Rectangle 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":80,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"Second\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"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\":[100,123.128,0],\"to\":[0,-3.333,0],\"ti\":[0,0,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":17,\"s\":[100,103.128,0],\"to\":[0,0,0],\"ti\":[0,-3.333,0]},{\"t\":36,\"s\":[100,123.128,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-24.016,23.834,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[10.968,36.17],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":20,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0.933333333333,0.78431372549,0.270588235294,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":5,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.933333393172,0.784313785329,0.270588235294,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\":[-24.117,25.326],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.141,19.847],\"ix\":1},\"s\":{\"a\":0,\"k\":[71.471,84.703],\"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\":\"Rectangle 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":80,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\"bg-second\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[124.016,100,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[10.968,36.17],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":20,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0.933333333333,0.78431372549,0.270588235294,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":5,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.933333393172,0.784313785329,0.270588235294,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\":[-24.117,25.326],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.141,19.847],\"ix\":1},\"s\":{\"a\":0,\"k\":[71.471,84.703],\"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\":\"Rectangle 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":80,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\"first\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"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\":12,\"s\":[75.984,124.378,0],\"to\":[0,-3.542,0],\"ti\":[0,0.833,0]},{\"i\":{\"x\":0.667,\"y\":1},\"o\":{\"x\":0.333,\"y\":0},\"t\":27,\"s\":[75.984,103.128,0],\"to\":[0,-0.821,0],\"ti\":[0,-0.037,0]},{\"t\":41,\"s\":[75.984,124,0]}],\"ix\":2},\"a\":{\"a\":0,\"k\":[-24.016,23.834,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[10.968,36.17],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":20,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0.933333333333,0.78431372549,0.270588235294,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":5,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.933333393172,0.784313785329,0.270588235294,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\":[-24.117,25.326],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.141,19.847],\"ix\":1},\"s\":{\"a\":0,\"k\":[71.471,84.703],\"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\":\"Rectangle 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":80,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\"bg-first\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[75.984,123.834,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[-24.016,23.834,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[10.968,36.17],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":20,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[0.933333333333,0.78431372549,0.270588235294,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":0,\"k\":5,\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"bm\":0,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.933333393172,0.784313785329,0.270588235294,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\":[-24.117,25.326],\"ix\":2},\"a\":{\"a\":0,\"k\":[-0.141,19.847],\"ix\":1},\"s\":{\"a\":0,\"k\":[71.471,84.703],\"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\":\"Rectangle 1\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":80,\"st\":0,\"bm\":0}],\"markers\":[]}"
  },
  {
    "path": "app/shared/src/main/res/raw/lottie_qrcode_background.json",
    "content": "{\"v\":\"5.12.1\",\"fr\":30,\"ip\":0,\"op\":91,\"w\":724,\"h\":724,\"nm\":\"SUW_QR_Ambient_Background_export\",\"ddd\":0,\"assets\":[{\"id\":\"comp_0\",\"nm\":\"Extra_Orbit_Shapes_dup_dup\",\"fr\":30,\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":3,\"nm\":\"Wiggle_Control\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[409.829,1025.829,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[331.829,335.829,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"ef\":[{\"ty\":5,\"nm\":\"freq\",\"np\":3,\"mn\":\"ADBE Slider Control\",\"ix\":1,\"en\":1,\"ef\":[{\"ty\":0,\"nm\":\"Slider\",\"mn\":\"ADBE Slider Control-0001\",\"ix\":1,\"v\":{\"a\":0,\"k\":0.5,\"ix\":1}}]},{\"ty\":5,\"nm\":\"amp\",\"np\":3,\"mn\":\"ADBE Slider Control\",\"ix\":2,\"en\":1,\"ef\":[{\"ty\":0,\"nm\":\"Slider\",\"mn\":\"ADBE Slider Control-0001\",\"ix\":1,\"v\":{\"a\":0,\"k\":20,\"ix\":1}}]},{\"ty\":5,\"nm\":\"loop\",\"np\":3,\"mn\":\"ADBE Slider Control\",\"ix\":3,\"en\":1,\"ef\":[{\"ty\":0,\"nm\":\"Slider\",\"mn\":\"ADBE Slider Control-0001\",\"ix\":1,\"v\":{\"a\":0,\"k\":3,\"ix\":1}}]}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[641.875,72.355,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.368,72.243,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.841,72.177,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.289,72.158,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.71,72.188,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.099,72.267,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.453,72.397,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.772,72.572,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.076,72.769,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.367,72.984,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.641,73.217,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.897,73.466,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.132,73.73,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.345,74.01,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.534,74.305,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.698,74.613,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.834,74.934,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.943,75.268,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.022,75.614,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.07,75.971,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.087,76.339,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.072,76.717,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.024,77.105,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.942,77.503,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.826,77.91,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.674,78.326,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.487,78.75,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.263,79.183,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.002,79.625,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.704,80.075,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.368,80.534,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.993,81.002,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.578,81.479,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.124,81.965,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.629,82.461,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.093,82.968,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.515,83.487,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.895,84.013,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.246,84.528,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.572,85.028,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.876,85.511,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.162,85.975,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.434,86.417,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.697,86.837,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.953,87.232,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.208,87.601,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.466,87.942,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.73,88.253,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.005,88.533,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.296,88.781,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.605,88.995,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.938,89.173,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.299,89.316,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.691,89.421,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.117,89.489,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.583,89.517,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.091,89.506,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.644,89.456,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.247,89.364,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.9,89.233,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.608,89.06,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.373,88.847,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.196,88.594,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.08,88.301,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.026,87.968,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.035,87.597,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.108,87.188,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.245,86.745,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.445,86.278,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.705,85.789,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.021,85.277,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.393,84.743,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.816,84.188,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.288,83.612,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.806,83.016,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.367,82.402,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.969,81.77,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.609,81.123,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.284,80.461,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.991,79.788,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.727,79.105,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.487,78.415,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.27,77.719,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.072,77.022,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.889,76.325,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.717,75.632,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.551,74.947,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.389,74.273,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.225,73.614,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.056,72.973,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.875,72.355,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"412\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[630.903,113.415,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.719,114.007,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.494,114.635,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.227,115.296,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.919,115.988,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.569,116.71,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.177,117.459,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.744,118.232,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.27,119.027,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.757,119.841,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.207,120.67,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.621,121.512,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.003,122.363,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.354,123.22,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.678,124.078,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.979,124.935,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.259,125.786,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.522,126.628,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.772,127.456,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.013,128.268,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.25,129.058,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.486,129.824,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.726,130.561,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.975,131.266,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.238,131.934,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.518,132.562,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.82,133.147,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.149,133.685,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.511,134.172,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.908,134.606,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.346,134.983,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.829,135.3,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.362,135.555,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.948,135.745,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.592,135.868,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.069,135.904,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.916,135.837,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.836,135.724,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.827,135.567,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.884,135.366,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.004,135.12,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.182,134.829,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.415,134.496,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.7,134.12,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.033,133.704,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.411,133.247,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.832,132.753,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.292,132.223,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.788,131.659,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.317,131.063,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.877,130.439,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.465,129.79,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.077,129.117,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.711,128.426,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.364,127.72,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.033,127.003,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.715,126.28,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.407,125.554,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.106,124.832,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.809,124.117,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.512,123.417,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.212,122.737,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.905,122.082,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.588,121.46,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.256,120.878,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.906,120.339,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.542,119.81,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.163,119.283,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.767,118.759,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.349,118.243,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.907,117.736,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.437,117.242,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.937,116.764,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.405,116.305,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.837,115.867,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.232,115.454,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.586,115.067,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.899,114.71,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.167,114.385,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.391,114.093,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.567,113.837,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.695,113.62,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.774,113.442,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.803,113.305,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.781,113.211,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.18,113.282,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.903,113.415,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"533\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":4,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[627.897,218.612,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.613,219.518,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.329,220.357,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.047,221.122,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.77,221.806,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.503,222.404,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.272,222.921,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.095,223.365,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.967,223.737,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.887,224.037,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.85,224.266,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.853,224.426,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.372,224.211,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.56,223.977,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.77,223.684,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.001,223.334,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.25,222.931,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.516,222.476,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.798,221.972,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.093,221.424,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.4,220.835,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.718,220.208,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.045,219.549,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.38,218.86,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.721,218.147,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.067,217.416,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.417,216.67,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.77,215.916,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.124,215.16,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.478,214.409,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.83,213.668,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.181,212.944,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.529,212.222,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.878,211.488,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.223,210.745,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.566,209.996,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.902,209.245,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.232,208.496,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.553,207.753,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.864,207.019,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.163,206.299,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.45,205.596,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.722,204.913,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.978,204.255,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.216,203.625,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.437,203.026,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.637,202.463,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.817,201.938,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.975,201.454,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.11,201.015,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.221,200.624,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.307,200.283,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.367,199.995,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.402,199.763,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.41,199.587,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.039,199.624,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.882,199.821,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.698,200.082,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.493,200.397,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.273,200.755,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.037,201.158,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.786,201.604,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.52,202.092,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.239,202.623,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.944,203.196,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.634,203.809,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.311,204.463,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.974,205.154,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.625,205.883,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.263,206.648,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.889,207.446,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.505,208.276,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.11,209.136,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.707,210.022,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.295,210.933,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.877,211.864,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.453,212.812,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.025,213.773,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.596,214.743,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.166,215.718,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.738,216.691,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.314,217.658,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.897,218.612,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"409\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":5,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[590.12,59.91,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[590.605,59.289,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[591.086,58.644,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[591.561,57.979,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[592.03,57.299,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[592.49,56.607,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[592.94,55.902,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[593.381,55.187,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[593.81,54.465,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[594.226,53.736,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[594.629,53.005,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[595.016,52.273,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[595.388,51.545,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[595.743,50.822,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.079,50.107,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.396,49.405,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.692,48.719,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.966,48.05,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.218,47.404,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.446,46.784,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.649,46.192,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.826,45.632,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.977,45.108,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.099,44.622,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.194,44.179,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.259,43.781,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.294,43.431,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.298,43.132,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.27,42.888,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.211,42.701,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.119,42.573,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.994,42.507,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.836,42.504,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.644,42.569,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.418,42.7,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.167,42.893,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.909,43.127,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.644,43.399,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.374,43.707,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.098,44.051,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[595.817,44.429,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[595.531,44.838,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[595.239,45.277,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[594.942,45.744,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[594.641,46.238,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[594.335,46.758,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[594.024,47.301,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[593.708,47.866,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[593.388,48.451,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[593.063,49.053,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[592.734,49.672,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[592.401,50.304,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[592.064,50.947,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[591.722,51.599,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[591.376,52.256,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[591.027,52.917,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[590.674,53.578,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[590.317,54.234,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.957,54.884,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.594,55.522,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.229,56.144,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.861,56.747,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.492,57.324,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.121,57.87,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[587.749,58.381,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[587.387,58.854,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[587.06,59.296,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.769,59.706,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.515,60.085,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.297,60.431,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.116,60.744,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.973,61.024,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.867,61.269,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.799,61.48,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.769,61.657,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.777,61.799,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.822,61.907,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.905,61.98,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.372,61.993,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.6,61.93,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.863,61.833,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[587.16,61.703,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[587.491,61.54,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[587.854,61.345,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.248,61.118,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.673,60.861,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.128,60.573,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.611,60.256,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[590.12,59.91,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"413\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":6,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[562.672,20.631,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.601,20.434,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.466,20.265,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.265,20.128,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.006,20.026,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.731,19.967,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.447,19.949,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.155,19.967,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[560.855,20.021,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[560.546,20.107,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[560.229,20.223,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[559.905,20.366,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[559.573,20.534,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[559.233,20.725,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[558.887,20.937,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[558.533,21.168,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[558.172,21.416,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[557.806,21.679,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[557.433,21.955,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[557.054,22.242,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[556.671,22.539,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[556.282,22.844,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[555.889,23.154,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[555.492,23.469,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[555.092,23.785,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[554.689,24.102,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[554.284,24.418,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[553.877,24.73,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[553.47,25.036,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[553.063,25.334,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[552.657,25.622,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[552.252,25.898,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[551.851,26.158,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[551.453,26.401,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[551.062,26.625,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[550.687,26.839,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[550.331,27.041,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[549.996,27.233,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[549.683,27.412,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[549.392,27.579,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[549.124,27.733,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.881,27.873,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.664,27.999,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.472,28.111,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.307,28.208,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.169,28.29,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.059,28.356,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.977,28.406,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.924,28.441,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.938,28.448,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.002,28.418,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.094,28.372,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.216,28.31,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.366,28.232,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.545,28.138,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.752,28.029,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.987,27.905,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[549.248,27.766,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[549.536,27.612,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[549.85,27.445,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[550.188,27.264,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[550.549,27.071,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[550.934,26.866,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[551.344,26.656,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[551.777,26.439,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[552.231,26.217,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[552.705,25.988,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[553.196,25.753,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[553.701,25.512,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[554.219,25.265,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[554.748,25.012,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[555.284,24.753,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[555.825,24.49,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[556.368,24.222,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[556.911,23.951,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[557.451,23.678,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[557.984,23.403,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[558.508,23.129,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[559.019,22.856,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[559.515,22.586,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[559.992,22.321,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[560.446,22.062,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[560.873,21.812,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.271,21.573,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.635,21.347,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.961,21.137,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.245,20.946,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.484,20.776,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.672,20.631,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"534\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":7,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[492.02,5.048,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.864,5.132,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.702,5.211,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.535,5.283,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.362,5.349,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.184,5.41,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.002,5.464,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.817,5.513,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.63,5.556,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.44,5.593,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.25,5.625,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.059,5.652,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.869,5.672,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.681,5.687,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.495,5.696,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.133,5.695,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.959,5.686,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.791,5.669,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.63,5.647,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.476,5.617,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.331,5.58,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.196,5.536,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.07,5.484,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.956,5.425,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.854,5.358,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.764,5.282,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.687,5.199,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.624,5.107,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.576,5.006,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.543,4.896,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.526,4.778,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.525,4.651,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.541,4.515,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.571,4.384,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.617,4.26,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.675,4.144,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.747,4.033,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.83,3.927,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.925,3.826,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.031,3.727,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.146,3.63,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.27,3.534,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.403,3.438,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.543,3.342,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.689,3.244,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.842,3.145,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489,3.043,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.162,2.938,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.327,2.83,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.495,2.718,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.664,2.602,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.833,2.482,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.001,2.357,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.168,2.229,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.331,2.096,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.49,1.959,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.643,1.818,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.788,1.674,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.925,1.527,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.052,1.377,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.166,1.226,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.267,1.075,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.352,0.924,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.431,0.792,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.506,0.685,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.578,0.603,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.646,0.546,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.71,0.513,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.771,0.505,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.828,0.52,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.88,0.559,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.928,0.622,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.972,0.707,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.011,0.815,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.046,0.944,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.077,1.095,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.103,1.267,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.124,1.458,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.14,1.669,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.152,1.899,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.16,2.147,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.162,2.412,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.16,2.693,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.154,2.99,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.143,3.301,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.127,3.627,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.107,3.965,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.082,4.315,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.053,4.676,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.02,5.048,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"34\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":8,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[639.535,558.852,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.637,558.378,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.076,557.831,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.566,557.757,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.951,557.738,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.505,557.744,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.048,557.757,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.593,557.773,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.157,557.789,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.569,557.808,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.981,557.825,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.359,557.993,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.333,558.498,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.915,558.896,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.435,559.094,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636,559.345,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.583,559.889,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.098,560.387,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.647,560.564,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.195,560.338,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.725,559.907,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.159,559.487,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1435\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":9,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[185.203,652.1,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.674,652.235,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.152,652.376,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.638,652.523,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.134,652.677,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.641,652.835,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.162,652.998,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.696,653.165,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.246,653.336,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.813,653.509,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.398,653.684,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.002,653.86,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.626,654.036,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.273,654.212,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.941,654.386,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.633,654.558,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.35,654.726,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.092,654.89,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.86,655.049,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.655,655.202,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.477,655.349,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.328,655.487,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.207,655.617,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.054,655.846,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.202,656.211,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.324,656.242,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.478,656.258,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.654,656.264,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.831,656.277,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.007,656.298,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.184,656.324,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.361,656.356,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.539,656.393,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.719,656.434,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.901,656.479,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.084,656.526,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.27,656.575,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.458,656.626,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.649,656.677,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.843,656.728,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.041,656.778,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.242,656.827,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.448,656.874,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.658,656.919,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.873,656.961,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.092,657,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.318,657.035,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.549,657.067,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.787,657.095,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.033,657.119,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.286,657.139,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.547,657.156,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.817,657.169,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.097,657.178,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.388,657.186,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.691,657.191,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.006,657.194,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.328,657.194,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.639,657.183,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.938,657.159,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.223,657.121,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.493,657.071,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.747,657.007,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.984,656.93,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.202,656.839,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.401,656.734,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.58,656.615,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.737,656.481,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.871,656.334,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.982,656.174,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.169,655.611,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.165,655.172,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.053,654.687,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.956,654.428,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.831,654.16,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.679,653.884,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.5,653.599,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.293,653.308,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.06,653.012,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.8,652.71,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.514,652.406,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.203,652.1,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1678\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":10,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[618.574,349.259,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.501,349.454,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.311,349.8,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.078,350.104,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.819,350.382,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.548,350.65,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.277,350.917,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.018,351.195,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.781,351.49,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.575,351.807,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.407,352.153,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.339,352.337,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.282,352.529,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.238,352.73,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.207,352.939,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.189,353.158,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.185,353.386,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.194,353.624,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.218,353.873,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.256,354.131,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.309,354.401,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.377,354.681,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.458,354.96,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.551,355.234,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.656,355.503,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.773,355.766,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.899,356.022,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.036,356.269,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.182,356.506,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.335,356.733,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.496,356.948,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.662,357.15,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.834,357.338,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.187,357.671,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.549,357.938,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.087,358.203,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.594,358.29,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.175,358.109,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.409,357.887,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.598,357.579,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.672,357.396,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.724,357.204,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.753,357.007,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.763,356.802,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.754,356.589,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.729,356.367,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.69,356.135,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.639,355.893,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.575,355.641,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.502,355.38,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.421,355.109,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.332,354.828,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.236,354.539,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.136,354.241,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.031,353.936,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.924,353.624,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.814,353.306,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.704,352.983,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.593,352.656,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.483,352.326,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.374,351.994,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.268,351.66,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.166,351.326,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.068,350.992,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.975,350.66,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.89,350.33,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.812,350.004,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.742,349.68,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.683,349.361,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"940\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":11,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[636.59,392.554,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.646,392.984,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.716,393.421,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.802,393.862,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.901,394.307,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.015,394.754,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.142,395.202,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.282,395.648,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.434,396.092,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.598,396.532,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.772,396.967,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.956,397.395,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.148,397.814,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.347,398.223,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.553,398.621,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.764,399.007,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.979,399.379,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.197,399.735,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.416,400.075,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.635,400.397,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.853,400.7,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.069,400.983,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.28,401.244,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.486,401.483,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.685,401.698,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.058,402.053,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.666,402.44,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.146,402.233,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.828,401.969,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.489,401.626,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.315,401.424,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.14,401.203,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.966,400.962,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.794,400.704,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.626,400.427,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.461,400.133,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.303,399.822,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.151,399.495,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.006,399.153,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.869,398.795,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.742,398.424,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.624,398.04,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.517,397.642,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.422,397.232,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.338,396.809,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.258,396.376,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.179,395.935,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.1,395.49,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.022,395.043,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.945,394.6,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.87,394.162,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.796,393.733,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.724,393.317,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.654,392.917,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.585,392.536,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.519,392.177,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.455,391.844,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.392,391.539,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.332,391.265,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.274,391.025,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.163,390.654,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.786,390.898,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.745,391.138,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.705,391.425,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.666,391.758,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.628,392.135,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.59,392.554,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1439\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":12,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[654.3,539.703,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.001,539.712,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[653.643,539.707,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[653.233,539.688,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.775,539.653,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.274,539.601,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[651.736,539.532,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[651.164,539.444,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[650.563,539.337,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.938,539.21,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.292,539.063,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.63,538.896,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.955,538.708,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.27,538.5,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.58,538.271,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.887,538.022,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.195,537.752,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.507,537.461,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.826,537.15,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.155,536.818,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.496,536.466,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.851,536.093,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.225,535.699,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.618,535.284,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.034,534.848,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.475,534.391,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.943,533.911,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.438,533.414,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.963,532.909,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.518,532.397,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.106,531.88,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.726,531.361,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.38,530.843,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.07,530.328,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.796,529.818,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.559,529.316,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.36,528.825,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.199,528.346,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.076,527.883,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.993,527.438,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.948,527.013,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.943,526.61,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.977,526.233,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.05,525.883,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.162,525.562,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.312,525.272,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.725,524.795,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.283,524.466,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.615,524.36,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.98,524.296,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.378,524.273,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.807,524.294,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.266,524.359,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.753,524.469,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.267,524.623,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.797,524.815,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.334,525.036,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.875,525.286,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.422,525.564,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.973,525.868,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.528,526.198,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.086,526.554,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.646,526.933,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.208,527.336,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.772,527.761,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.336,528.207,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.9,528.673,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.462,529.158,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.023,529.66,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.581,530.18,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.134,530.714,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.683,531.262,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.224,531.824,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.758,532.396,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[650.282,532.979,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[650.795,533.57,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[651.295,534.168,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[651.779,534.772,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.246,535.38,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.693,535.99,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[653.118,536.602,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[653.518,537.212,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[653.89,537.821,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.231,538.425,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.537,539.024,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.793,539.619,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1434\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":13,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[603.337,648.541,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[602.256,647.901,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[601.195,647.255,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[600.158,646.605,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[599.147,645.953,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.167,645.302,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.219,644.654,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.307,644.01,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[595.435,643.373,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[594.605,642.746,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[593.82,642.13,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[593.083,641.528,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[592.396,640.942,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[591.763,640.374,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[591.185,639.827,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[590.665,639.303,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[590.204,638.805,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.806,638.334,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.472,637.893,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.204,637.484,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.003,637.109,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.871,636.77,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.82,636.21,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.287,635.692,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.591,635.612,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.969,635.581,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[590.407,635.595,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[590.859,635.634,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[591.323,635.696,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[591.798,635.781,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[592.286,635.885,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[592.785,636.009,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[593.295,636.152,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[593.817,636.311,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[594.35,636.486,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[594.894,636.677,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[595.447,636.882,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.011,637.101,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[596.584,637.333,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.165,637.578,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[597.754,637.834,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.349,638.102,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[598.951,638.382,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[599.556,638.672,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[600.165,638.974,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[600.776,639.286,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[601.387,639.609,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[601.996,639.943,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[602.603,640.288,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[603.203,640.645,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[603.797,641.012,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[604.38,641.392,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[604.951,641.784,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[605.507,642.189,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[606.045,642.608,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[606.562,643.04,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[607.053,643.484,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[607.509,643.929,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[607.929,644.372,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[608.311,644.811,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[608.656,645.243,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[608.963,645.667,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[609.232,646.081,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[609.461,646.482,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[609.65,646.869,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[609.799,647.24,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[609.909,647.593,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.006,648.238,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[609.942,648.791,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[609.546,649.42,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[609.084,649.692,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[608.796,649.781,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[608.47,649.838,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[608.107,649.861,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[607.708,649.85,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[607.274,649.805,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[606.806,649.726,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[606.304,649.613,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[605.77,649.465,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[605.205,649.284,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[604.61,649.069,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[603.987,648.821,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[603.337,648.541,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":18,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1639\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":14,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[516.076,621.885,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.051,622.325,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.075,622.842,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.159,623.419,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.225,623.726,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.309,624.043,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.411,624.37,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.533,624.703,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.673,625.044,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.834,625.39,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.015,625.741,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.216,626.096,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.438,626.455,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.68,626.818,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.942,627.185,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.225,627.554,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.527,627.928,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.848,628.305,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.187,628.687,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.545,629.074,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.918,629.466,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[520.307,629.865,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[520.707,630.259,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[521.116,630.646,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[521.532,631.023,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[521.953,631.389,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[522.376,631.744,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[522.801,632.084,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[523.224,632.41,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[523.643,632.719,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[524.058,633.01,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[524.464,633.282,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[524.861,633.534,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[525.246,633.765,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[525.617,633.973,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[525.972,634.157,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[526.308,634.317,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[526.625,634.451,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[526.919,634.559,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[527.434,634.692,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[527.84,634.711,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[528.301,634.24,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[528.241,633.842,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[528.034,633.328,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[527.689,632.716,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[527.467,632.379,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[527.213,632.025,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[526.928,631.654,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[526.612,631.269,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[526.268,630.87,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[525.896,630.46,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[525.497,630.039,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[525.074,629.609,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[524.629,629.171,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[524.164,628.728,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[523.681,628.279,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[523.183,627.827,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[522.673,627.373,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[522.154,626.919,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[521.63,626.467,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[521.105,626.017,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[520.582,625.572,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[520.066,625.134,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.563,624.705,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.076,624.287,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.612,623.883,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.177,623.495,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.775,623.126,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.415,622.779,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.102,622.458,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.651,621.905,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":18,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1552\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":15,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[500.702,642.523,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.775,642.862,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.992,643.476,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.299,643.999,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.687,644.426,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.144,644.751,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.394,644.875,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.656,644.973,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.929,645.044,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.209,645.088,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.496,645.105,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.788,645.096,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[504.081,645.059,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[504.375,644.996,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[504.666,644.907,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[504.953,644.791,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[505.235,644.649,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[505.508,644.481,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[505.77,644.287,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[506.257,643.827,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[506.681,643.271,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[507.054,642.671,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[507.386,642.069,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[507.672,641.476,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[507.907,640.9,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[508.088,640.346,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[508.212,639.82,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[508.282,639.324,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[508.3,638.861,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[508.244,638.231,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[508.123,637.68,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[507.953,637.067,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[507.972,636.451,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[507.558,636.632,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[507.098,637.008,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[506.707,637.342,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[506.254,637.73,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[506.007,637.941,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[505.747,638.163,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[505.475,638.392,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[505.191,638.63,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[504.897,638.873,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[504.595,639.121,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[504.284,639.373,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.966,639.628,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.642,639.885,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.313,640.143,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.98,640.401,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.645,640.659,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.309,640.915,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.973,641.17,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.638,641.422,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.306,641.673,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.978,641.92,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1387\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":16,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[148.199,632.188,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.543,631.847,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.901,631.518,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[149.271,631.201,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[149.651,630.896,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[150.039,630.605,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[150.433,630.327,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[150.83,630.063,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[151.229,629.813,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[151.628,629.577,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[152.025,629.355,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[152.418,629.147,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[152.805,628.953,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[153.186,628.772,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[153.558,628.605,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[153.92,628.45,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[154.27,628.306,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[154.609,628.174,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[154.933,628.052,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[155.244,627.94,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[155.54,627.835,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[155.819,627.738,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[156.083,627.647,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[156.329,627.56,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[156.559,627.476,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[156.772,627.394,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[156.969,627.312,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[157.144,627.236,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[157.295,627.176,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[157.419,627.132,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[157.518,627.105,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[157.405,627.45,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[157.286,627.556,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[157.146,627.674,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[156.987,627.803,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[156.809,627.942,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[156.613,628.091,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[156.401,628.248,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[156.175,628.414,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[155.934,628.585,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[155.68,628.763,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[155.416,628.946,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[155.142,629.132,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[154.859,629.322,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[154.569,629.513,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[154.273,629.706,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[153.973,629.898,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[153.67,630.089,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[153.365,630.279,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[153.06,630.466,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[152.756,630.651,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[152.452,630.836,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[152.152,631.02,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[151.856,631.202,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[151.567,631.382,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[151.286,631.559,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[151.014,631.731,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[150.753,631.899,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[150.504,632.06,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[150.267,632.215,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[150.044,632.363,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[149.835,632.502,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[149.64,632.633,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[149.461,632.753,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[149.296,632.863,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[149.147,632.962,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[149.012,633.048,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.892,633.122,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.786,633.182,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.694,633.228,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.614,633.259,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.487,633.273,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.357,633.165,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.216,632.753,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1680\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":17,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[380.513,630.304,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[380.386,629.824,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[380.033,629.228,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[379.702,628.92,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[379.512,628.794,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[379.308,628.686,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[379.09,628.596,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[378.862,628.526,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[378.625,628.474,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[378.38,628.441,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[378.13,628.426,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[377.876,628.429,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[377.621,628.45,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[377.366,628.488,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[377.114,628.544,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[376.866,628.616,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[376.624,628.704,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[376.39,628.807,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[376.167,628.926,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[375.956,629.059,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[375.754,629.207,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[375.552,629.375,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[375.35,629.561,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[375.152,629.764,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[374.96,629.982,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[374.603,630.454,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[374.295,630.963,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[374.05,631.495,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[373.875,632.036,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[373.776,632.576,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[373.755,633.101,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[373.809,633.603,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[373.932,634.071,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[374.221,634.69,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[374.586,635.186,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[374.953,635.538,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[375.195,635.684,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[375.5,635.763,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[375.861,635.776,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[376.059,635.757,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[376.266,635.723,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[376.481,635.673,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[376.704,635.608,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[376.931,635.528,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[377.162,635.434,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[377.395,635.326,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[377.629,635.205,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[377.862,635.071,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[378.093,634.925,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[378.32,634.767,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[378.542,634.599,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[378.757,634.421,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[378.965,634.234,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[379.165,634.038,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[379.532,633.623,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[379.851,633.183,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[380.114,632.725,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[380.318,632.256,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[380.458,631.781,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[380.529,631.31,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1679\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":18,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[25.468,526.14,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.235,526.436,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.011,526.776,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.798,527.156,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.598,527.572,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.413,528.021,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.247,528.499,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.1,529.002,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.974,529.527,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.87,530.071,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.79,530.631,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.733,531.204,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.701,531.787,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.693,532.377,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.709,532.971,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.748,533.568,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.811,534.165,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.895,534.759,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.999,535.349,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.123,535.932,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.263,536.507,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.417,537.072,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.583,537.625,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.758,538.164,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.939,538.689,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.122,539.197,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.319,539.687,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.531,540.155,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.756,540.6,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.994,541.022,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.243,541.417,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.502,541.786,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.77,542.127,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.045,542.438,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.325,542.719,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.61,542.969,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.897,543.187,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.186,543.371,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.473,543.522,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.759,543.639,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.04,543.721,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.316,543.768,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.585,543.779,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.845,543.755,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.095,543.695,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.333,543.6,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.557,543.469,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.767,543.302,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.961,543.101,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.138,542.865,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.295,542.595,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.433,542.291,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.551,541.956,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.646,541.588,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.719,541.19,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.767,540.765,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.772,540.34,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.732,539.919,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.652,539.502,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.534,539.086,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.381,538.67,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.198,538.254,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.987,537.836,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.751,537.414,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.494,536.989,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.219,536.56,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.929,536.125,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.626,535.686,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.314,535.241,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.997,534.79,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.676,534.334,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.355,533.873,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.036,533.407,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.724,532.938,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.42,532.465,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.128,531.99,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.85,531.514,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.589,531.038,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.349,530.565,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.132,530.095,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.941,529.631,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.779,529.176,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.65,528.73,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.555,528.298,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.499,527.882,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.48,527.487,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.47,527.131,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.462,526.82,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.459,526.325,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1686\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":19,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[17.855,498.57,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.15,499.105,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.465,499.607,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.798,500.073,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.147,500.503,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.51,500.895,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.887,501.249,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.274,501.562,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.671,501.834,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.074,502.064,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.483,502.25,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.895,502.392,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.307,502.488,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.719,502.537,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.127,502.54,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.53,502.495,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.926,502.401,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.313,502.259,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.688,502.067,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.049,501.825,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.396,501.534,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.725,501.192,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.035,500.801,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.324,500.359,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.59,499.869,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.807,499.393,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.961,498.968,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.057,498.59,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.098,498.254,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.088,497.954,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.03,497.688,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.929,497.451,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.786,497.238,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.607,497.046,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.394,496.872,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.151,496.711,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.88,496.561,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.587,496.419,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.273,496.281,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.941,496.145,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.596,496.009,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.241,495.87,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.878,495.727,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.511,495.578,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.143,495.421,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.778,495.256,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.418,495.08,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.066,494.894,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.727,494.698,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.403,494.49,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.097,494.271,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.814,494.042,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.555,493.804,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.325,493.556,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.127,493.302,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.938,493.063,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.744,492.851,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.547,492.667,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.348,492.51,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.148,492.38,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.95,492.275,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.754,492.196,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.561,492.141,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.374,492.111,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.192,492.105,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.018,492.122,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.852,492.161,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.696,492.223,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.55,492.306,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.416,492.41,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.294,492.535,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.184,492.679,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.089,492.842,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.007,493.024,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.94,493.225,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.889,493.443,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.852,493.678,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.832,493.93,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.827,494.198,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.837,494.483,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.863,494.782,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.905,495.097,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.961,495.427,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.032,495.771,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.116,496.129,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.212,496.505,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.318,496.897,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.436,497.303,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.564,497.719,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.704,498.143,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.855,498.57,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1315\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":20,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[441.897,638.31,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[442.134,638.159,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[442.516,637.994,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[442.896,638.033,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[443.169,638.622,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[443.209,639.138,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[443.221,639.678,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[443.281,640.312,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[443.42,640.948,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[443.381,641.505,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[443.211,642.074,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[442.935,642.611,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[442.495,643.162,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[442.12,643.447,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[441.692,643.534,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[441.275,643.604,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[440.887,643.823,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[440.5,644.049,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[440.07,643.984,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[440.131,643.38,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[440.283,642.848,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[440.394,642.513,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[440.529,642.129,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[440.689,641.692,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[440.875,641.201,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[441.066,640.688,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[441.226,640.218,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[441.357,639.791,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[441.461,639.406,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[441.593,638.767,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1669\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":21,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[131.395,665.879,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.693,666.402,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.981,666.917,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.259,667.422,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.526,667.916,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.782,668.397,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.025,668.862,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.256,669.309,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.474,669.737,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.68,670.143,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.873,670.526,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.053,670.882,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.22,671.211,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.376,671.509,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.519,671.775,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.651,672.006,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.773,672.202,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.883,672.359,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.985,672.475,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.077,672.55,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.161,672.581,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.238,672.566,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.309,672.505,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.434,672.275,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.534,671.975,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.614,671.414,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.612,670.973,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.565,670.484,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.475,669.95,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.346,669.375,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.269,669.072,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.185,668.761,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.096,668.441,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.003,668.112,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.907,667.775,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.81,667.43,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.713,667.078,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.617,666.72,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.526,666.355,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.439,665.984,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.359,665.608,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.288,665.228,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.226,664.844,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.176,664.456,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.14,664.067,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.113,663.678,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.073,663.304,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.018,662.945,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.951,662.603,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.871,662.28,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.78,661.976,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.679,661.693,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.569,661.432,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.452,661.193,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.329,660.978,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.2,660.788,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.068,660.622,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.933,660.483,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.796,660.37,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.66,660.285,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.525,660.227,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.392,660.197,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.262,660.196,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.137,660.224,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.017,660.28,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.903,660.366,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.797,660.481,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.698,660.624,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.609,660.797,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.529,660.998,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.398,661.484,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.309,662.077,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.281,662.413,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.263,662.773,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.251,663.157,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.248,663.562,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.253,663.989,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.269,664.436,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.297,664.901,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.339,665.382,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1685\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":22,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[31.742,476.677,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.502,476.499,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.267,476.288,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.038,476.048,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.814,475.778,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.597,475.482,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.387,475.16,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.185,474.814,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.99,474.447,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.804,474.06,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.627,473.655,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.459,473.233,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.299,472.796,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.149,472.348,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.009,471.888,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.878,471.42,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.755,470.946,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.642,470.468,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.538,469.988,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.442,469.509,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.354,469.032,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.273,468.561,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.199,468.098,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.137,467.641,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.091,467.19,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.059,466.748,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.042,466.316,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.04,465.896,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.052,465.489,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.078,465.097,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.118,464.722,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.171,464.365,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.237,464.028,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.315,463.713,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.404,463.42,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.503,463.152,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.613,462.909,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.731,462.693,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.858,462.505,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.992,462.347,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.133,462.219,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.279,462.121,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.43,462.056,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.584,462.024,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.741,462.026,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.9,462.061,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.059,462.131,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.218,462.236,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.376,462.375,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.532,462.55,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.685,462.76,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.834,463.004,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.979,463.282,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.127,463.59,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.277,463.925,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.427,464.286,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.575,464.672,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.72,465.08,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.859,465.508,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.99,465.956,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.113,466.422,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.226,466.903,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.328,467.398,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.418,467.906,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.495,468.423,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.559,468.948,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.609,469.478,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.646,470.012,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.669,470.547,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.678,471.079,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.674,471.607,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.656,472.128,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.627,472.638,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.587,473.134,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.536,473.612,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.476,474.07,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.408,474.503,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.335,474.906,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.257,475.276,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.176,475.609,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.095,475.899,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.015,476.141,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.94,476.331,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.874,476.483,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.818,476.603,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.773,476.691,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.74,476.747,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1233\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":23,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[207.643,34.392,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.689,34.058,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.77,33.741,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.882,33.441,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[208.025,33.16,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[208.198,32.9,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[208.398,32.662,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[208.623,32.447,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[208.872,32.256,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[209.141,32.091,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[209.428,31.952,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[209.73,31.841,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[210.045,31.759,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[210.369,31.707,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[210.7,31.685,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[211.034,31.694,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[211.369,31.736,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[211.7,31.81,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.025,31.917,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.34,32.058,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.642,32.233,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.929,32.442,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.234,32.678,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.583,32.934,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.968,33.209,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[214.381,33.501,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[214.816,33.809,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[215.267,34.132,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[215.727,34.469,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.192,34.818,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.656,35.177,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.113,35.546,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.56,35.924,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.992,36.308,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.404,36.698,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.795,37.092,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.159,37.489,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.494,37.886,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.797,38.282,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.067,38.675,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.301,39.063,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.498,39.444,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.655,39.817,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.773,40.177,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.851,40.523,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.888,40.852,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.885,41.162,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.842,41.448,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.759,41.708,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.638,41.938,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.48,42.135,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.287,42.294,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.057,42.419,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.79,42.517,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.488,42.587,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.154,42.629,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.789,42.646,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.396,42.636,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.978,42.601,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.538,42.542,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.078,42.46,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.602,42.354,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.112,42.228,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[215.612,42.08,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[215.104,41.913,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[214.591,41.728,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[214.077,41.525,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.564,41.307,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.056,41.073,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.555,40.825,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.065,40.565,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[211.588,40.293,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[211.126,40.011,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[210.683,39.719,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[210.26,39.42,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[209.861,39.114,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[209.487,38.803,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[209.14,38.487,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[208.822,38.168,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[208.536,37.846,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[208.281,37.523,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[208.06,37.199,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.872,36.875,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.718,36.551,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.598,36.229,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.514,35.909,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.466,35.593,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.455,35.282,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.481,34.977,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.544,34.68,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[207.643,34.392,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"258\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":24,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[127.684,53.192,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[128.315,53.427,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[128.951,53.647,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[129.588,53.851,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[130.219,54.041,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[130.841,54.217,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.449,54.38,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.039,54.53,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.607,54.669,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.149,54.795,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.663,54.91,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.146,55.014,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.595,55.106,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.009,55.186,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.384,55.255,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.72,55.312,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.016,55.355,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.271,55.385,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.483,55.4,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.653,55.399,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.781,55.381,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.866,55.343,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.904,55.285,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.897,55.208,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.844,55.112,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.746,54.998,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.606,54.866,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.423,54.716,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.2,54.551,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.939,54.37,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.641,54.175,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[135.309,53.968,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.944,53.748,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.55,53.518,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[134.128,53.279,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.682,53.032,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.214,52.78,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.727,52.522,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.223,52.262,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.707,52,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.181,51.739,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[130.647,51.479,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[130.109,51.222,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[129.57,50.971,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[129.032,50.726,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[128.5,50.49,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[127.975,50.263,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[127.461,50.048,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[126.96,49.845,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[126.475,49.656,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[126.009,49.483,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[125.563,49.324,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[125.137,49.171,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.732,49.024,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.351,48.883,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.996,48.747,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.667,48.618,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.367,48.495,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.096,48.379,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.854,48.27,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.643,48.169,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.462,48.078,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.312,47.996,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.193,47.925,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.104,47.866,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.044,47.821,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.159,47.843,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.254,47.908,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.369,47.997,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.502,48.112,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.65,48.254,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.811,48.426,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.981,48.629,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.159,48.865,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.339,49.135,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.52,49.443,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.697,49.788,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.89,50.159,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.138,50.527,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.439,50.89,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.789,51.246,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[125.183,51.595,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[125.618,51.935,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[126.09,52.266,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[126.595,52.587,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[127.127,52.896,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[127.684,53.192,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1696\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":25,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[131.958,20.884,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.66,20.582,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.341,20.284,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.005,19.989,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[130.652,19.7,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[130.284,19.419,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[129.903,19.146,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[129.513,18.885,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[129.114,18.637,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[128.71,18.404,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[128.304,18.187,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[127.896,17.99,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[127.491,17.814,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[127.092,17.662,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[126.7,17.535,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[126.318,17.435,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[125.95,17.366,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[125.598,17.329,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[125.266,17.326,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.955,17.359,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.667,17.428,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.398,17.516,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.147,17.619,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.914,17.732,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.7,17.855,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.504,17.985,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.327,18.12,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.169,18.258,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.031,18.399,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.912,18.541,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.813,18.683,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.734,18.825,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.675,18.967,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.637,19.107,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.618,19.248,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.621,19.388,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.644,19.528,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.687,19.669,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.752,19.811,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.837,19.956,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[122.943,20.105,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.071,20.258,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.219,20.417,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.388,20.584,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.578,20.759,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[123.789,20.945,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.022,21.143,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.275,21.354,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.55,21.581,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[124.846,21.825,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[125.162,22.083,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[125.5,22.331,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[125.855,22.566,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[126.226,22.788,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[126.61,22.995,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[127.005,23.188,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[127.409,23.366,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[127.819,23.53,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[128.232,23.678,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[128.647,23.81,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[129.06,23.928,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[129.469,24.029,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[129.872,24.116,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[130.266,24.187,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[130.649,24.242,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.019,24.282,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.373,24.307,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.71,24.316,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.028,24.31,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.324,24.29,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.596,24.255,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.844,24.206,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.066,24.142,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.26,24.065,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.425,23.974,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.56,23.869,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.664,23.752,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.737,23.621,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.778,23.478,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.787,23.323,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.765,23.155,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.718,22.968,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.645,22.766,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.543,22.55,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.413,22.324,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.252,22.09,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[133.059,21.85,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.834,21.606,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.576,21.363,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[132.284,21.121,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[131.958,20.884,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"580\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":26,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[44.15,282.978,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.295,283.239,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.447,283.493,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.604,283.738,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.767,283.975,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.935,284.201,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.108,284.416,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.285,284.619,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.467,284.81,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.654,284.988,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.845,285.153,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.042,285.306,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.246,285.445,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.456,285.571,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.673,285.685,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.899,285.786,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.135,285.875,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.383,285.952,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.642,286.017,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.916,286.072,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.197,286.112,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.482,286.136,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.769,286.144,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.059,286.136,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.35,286.112,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.639,286.073,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.927,286.018,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[50.211,285.948,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[50.491,285.864,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[50.765,285.766,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.031,285.654,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.288,285.529,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.535,285.392,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.77,285.243,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.992,285.083,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.199,284.913,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.391,284.733,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.566,284.545,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.722,284.349,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.859,284.146,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.975,283.937,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.069,283.723,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.141,283.505,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.189,283.285,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.213,283.062,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.211,282.838,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.183,282.615,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.129,282.393,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.048,282.173,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.94,281.955,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.826,281.724,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.709,281.475,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.587,281.211,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.459,280.936,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.324,280.652,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.179,280.36,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.024,280.065,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.857,279.768,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.678,279.472,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.485,279.178,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.279,278.89,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.058,278.609,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[50.823,278.338,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[50.574,278.08,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[50.311,277.835,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[50.035,277.608,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.746,277.399,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.446,277.212,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.136,277.049,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.817,276.912,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.491,276.805,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.161,276.729,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.829,276.688,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.497,276.685,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.168,276.722,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.847,276.803,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.535,276.931,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.237,277.111,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.958,277.344,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.701,277.636,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.465,277.974,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.248,278.352,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.049,278.767,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.87,279.216,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.71,279.693,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.569,280.197,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.447,280.723,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.344,281.267,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.26,281.827,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.196,282.398,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.15,282.978,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1690\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":27,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[55.557,403.951,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.554,403.704,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.55,403.44,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.544,403.161,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.534,402.868,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.521,402.563,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.504,402.245,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.482,401.918,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.453,401.583,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.418,401.242,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.376,400.895,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.325,400.546,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.264,400.195,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.194,399.845,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.113,399.497,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.02,399.155,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.915,398.819,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.797,398.491,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.681,398.169,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.579,397.847,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.49,397.528,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.411,397.212,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.341,396.901,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.278,396.595,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.22,396.296,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.165,396.004,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.112,395.722,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.06,395.449,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.006,395.186,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.951,394.935,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.893,394.697,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.83,394.472,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.763,394.262,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.691,394.068,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.612,393.89,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.528,393.731,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.437,393.59,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.34,393.47,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.237,393.371,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.129,393.296,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.015,393.245,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.898,393.22,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.777,393.224,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.654,393.256,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.531,393.321,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.409,393.419,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.29,393.553,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.176,393.724,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.07,393.932,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.974,394.171,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.888,394.44,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.812,394.736,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.745,395.056,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.689,395.4,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.643,395.764,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.607,396.145,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.582,396.542,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.566,396.952,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.561,397.372,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.566,397.8,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.582,398.233,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.607,398.668,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.642,399.103,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.687,399.536,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.742,399.964,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.807,400.384,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.881,400.795,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[51.964,401.193,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.057,401.578,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.158,401.946,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.268,402.296,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.387,402.626,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.513,402.934,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.648,403.219,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.79,403.48,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[52.939,403.715,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.095,403.923,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.258,404.104,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.429,404.263,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.608,404.403,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.795,404.52,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[53.988,404.612,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.186,404.677,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.387,404.711,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.589,404.713,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.791,404.681,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[54.991,404.613,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.187,404.507,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.376,404.363,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[55.558,404.18,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1027\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":28,\"ty\":4,\"nm\":\".dot1\",\"cl\":\"dot1\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[42.489,58.809,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.542,58.889,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.602,58.992,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.666,59.115,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.735,59.259,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.804,59.422,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.875,59.603,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.944,59.801,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.011,60.017,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.075,60.249,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.136,60.496,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.192,60.76,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.244,61.038,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.291,61.331,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.333,61.639,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.371,61.962,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.405,62.3,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.435,62.653,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.463,63.021,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.487,63.401,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.51,63.792,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.53,64.193,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.547,64.602,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.562,65.017,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.575,65.437,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.585,65.859,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.592,66.282,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.598,66.703,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.6,67.12,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.601,67.532,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.599,67.936,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.594,68.33,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.587,68.712,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.577,69.079,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.565,69.43,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.551,69.763,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.534,70.074,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.514,70.362,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.491,70.626,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.466,70.863,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.439,71.071,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.409,71.249,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.376,71.394,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.34,71.506,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.302,71.582,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.261,71.622,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.217,71.624,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.171,71.587,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.127,71.524,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.097,71.463,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.081,71.403,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.08,71.341,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.093,71.273,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.119,71.198,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.158,71.113,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.209,71.015,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.27,70.903,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.342,70.774,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.421,70.628,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.508,70.462,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.6,70.275,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.697,70.067,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.795,69.836,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.894,69.582,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.992,69.305,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.086,69.004,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.176,68.68,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.258,68.333,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.332,67.964,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.395,67.574,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.444,67.163,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.479,66.734,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.497,66.288,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.497,65.827,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.475,65.353,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.431,64.87,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.363,64.379,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.269,63.885,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.156,63.394,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.044,62.913,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.932,62.446,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.819,61.994,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.702,61.557,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.58,61.138,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.453,60.738,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.317,60.358,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.174,59.999,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.02,59.664,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.855,59.353,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.678,59.067,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.489,58.809,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.878431379795,0.89411765337,0.858823537827,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"50\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":29,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[411.369,39.06,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.456,39.712,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.518,40.345,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.552,40.955,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.56,41.539,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.54,42.095,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.492,42.617,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.417,43.104,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.315,43.551,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.185,43.956,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.027,44.315,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[410.841,44.625,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[410.627,44.882,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[410.387,45.084,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[410.118,45.228,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[409.823,45.311,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[409.5,45.33,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[409.153,45.29,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[408.799,45.235,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[408.444,45.17,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[408.092,45.094,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[407.745,45.005,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[407.406,44.904,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[407.079,44.787,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[406.764,44.655,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[406.465,44.507,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[406.182,44.342,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[405.917,44.159,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[405.67,43.957,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[405.443,43.737,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[405.236,43.499,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[405.05,43.241,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.883,42.966,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.737,42.672,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.611,42.361,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.504,42.033,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.416,41.689,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.345,41.331,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.291,40.96,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.253,40.577,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.229,40.185,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.219,39.787,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.22,39.383,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.231,38.978,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.252,38.574,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.279,38.174,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.312,37.782,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.353,37.402,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.42,37.026,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.517,36.656,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.641,36.293,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.79,35.939,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[404.964,35.594,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[405.159,35.261,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[405.374,34.941,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[405.607,34.634,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[405.856,34.344,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[406.118,34.07,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[406.391,33.814,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[406.674,33.578,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[406.963,33.362,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[407.257,33.168,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[407.554,32.997,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[407.851,32.85,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[408.146,32.728,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[408.438,32.631,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[408.724,32.561,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[409.003,32.519,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[409.273,32.504,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[409.531,32.518,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[409.778,32.561,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[410.01,32.633,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[410.228,32.735,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[410.429,32.866,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[410.614,33.027,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[410.78,33.217,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[410.929,33.437,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.061,33.684,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.186,33.958,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.304,34.258,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.411,34.583,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.505,34.935,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.583,35.311,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.641,35.713,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.679,36.137,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.695,36.583,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.685,37.049,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.649,37.532,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.585,38.03,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.492,38.541,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[411.369,39.06,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"25\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":30,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[25.656,84.16,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.138,84.031,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.648,83.877,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.188,83.702,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.758,83.506,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.36,83.291,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.995,83.06,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.664,82.814,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.366,82.555,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.102,82.287,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.872,82.01,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.676,81.729,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.513,81.446,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.383,81.164,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.286,80.886,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.22,80.617,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.184,80.359,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.186,80.107,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.227,79.857,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.308,79.611,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.428,79.368,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.586,79.131,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.78,78.901,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.011,78.678,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.275,78.463,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.573,78.257,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.901,78.062,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.259,77.879,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.643,77.708,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.052,77.551,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.483,77.408,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.934,77.281,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.403,77.17,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.885,77.077,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.38,77.002,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.883,76.945,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.393,76.908,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.906,76.891,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.419,76.896,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.929,76.921,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.434,76.969,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.931,77.039,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.416,77.131,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.887,77.246,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.341,77.384,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.775,77.544,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.188,77.727,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.603,77.928,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.028,78.145,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.457,78.377,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.886,78.626,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.312,78.89,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.731,79.17,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.138,79.465,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.53,79.774,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.904,80.098,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.257,80.435,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.585,80.784,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.887,81.144,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.16,81.513,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.401,81.89,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.607,82.273,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.778,82.66,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.91,83.048,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.003,83.435,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.053,83.819,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.06,84.196,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.022,84.563,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.937,84.916,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.804,85.252,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.622,85.566,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.389,85.853,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.104,86.109,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.766,86.329,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.373,86.507,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.925,86.637,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.421,86.714,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.867,86.747,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.27,86.742,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.636,86.699,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.97,86.621,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.276,86.508,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.56,86.362,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.828,86.184,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.084,85.975,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.333,85.737,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.581,85.471,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.833,85.179,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.093,84.862,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.366,84.522,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.656,84.16,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1693\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":31,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[25.733,432.397,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.286,432.552,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.842,432.728,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.4,432.924,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.957,433.141,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.508,433.379,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.051,433.637,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.582,433.917,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.098,434.219,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.596,434.541,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.07,434.884,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.519,435.247,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.937,435.632,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.323,436.037,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.671,436.461,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.978,436.906,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.258,437.36,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.527,437.812,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.783,438.261,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.026,438.706,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.255,439.147,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.469,439.583,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.667,440.014,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.848,440.438,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.011,440.856,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.155,441.267,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.28,441.671,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.385,442.068,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.469,442.456,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.531,442.836,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.571,443.207,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.588,443.569,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.58,443.92,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.549,444.261,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.492,444.59,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.409,444.906,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.3,445.208,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.164,445.495,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.001,445.765,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.809,446.017,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.589,446.248,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.339,446.456,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.06,446.64,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.75,446.796,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.409,446.921,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.037,447.012,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.637,447.063,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.212,447.07,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.766,447.035,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.301,446.957,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.821,446.841,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.327,446.685,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.823,446.494,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.312,446.267,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.796,446.008,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.278,445.719,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.762,445.4,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.251,445.056,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.747,444.687,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.253,444.297,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.771,443.887,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.306,443.46,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.859,443.018,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.433,442.564,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.031,442.1,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.655,441.628,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.307,441.151,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.989,440.671,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.703,440.19,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.451,439.709,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.236,439.233,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.057,438.761,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.916,438.296,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.815,437.84,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.754,437.395,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.733,436.962,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.751,436.537,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.806,436.116,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.897,435.703,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.026,435.302,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.191,434.914,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.393,434.544,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.632,434.193,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.907,433.865,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.216,433.561,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.56,433.285,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.936,433.039,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.343,432.825,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.78,432.646,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.244,432.503,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.733,432.397,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1149\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":32,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[42.904,186.997,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.644,187.337,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.37,187.658,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.082,187.959,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.782,188.24,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.47,188.501,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.149,188.743,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.818,188.963,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.48,189.164,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.136,189.343,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.786,189.501,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.431,189.637,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.072,189.751,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.711,189.841,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.348,189.907,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.984,189.947,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.62,189.958,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.258,189.941,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.9,189.894,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.546,189.82,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.198,189.717,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.857,189.587,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.526,189.431,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.205,189.25,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.896,189.044,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.601,188.817,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.321,188.568,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.058,188.3,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.812,188.014,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.586,187.712,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.381,187.397,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.198,187.069,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.038,186.732,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.903,186.388,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.794,186.038,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.711,185.685,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.657,185.332,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.631,184.98,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.635,184.632,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.669,184.29,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.733,183.956,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.829,183.633,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.957,183.323,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.116,183.028,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.308,182.75,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.524,182.482,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.746,182.201,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.974,181.907,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.209,181.606,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.453,181.301,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.707,180.994,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.972,180.69,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.247,180.392,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.535,180.102,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.834,179.824,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.146,179.56,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.47,179.313,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.805,179.085,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.151,178.88,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.507,178.698,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.872,178.543,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.245,178.415,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.623,178.317,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.005,178.251,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.389,178.218,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.771,178.22,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.149,178.257,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.519,178.33,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.878,178.441,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.221,178.59,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.544,178.777,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.842,179.004,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.11,179.27,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.342,179.576,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.532,179.92,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.681,180.299,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.808,180.698,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.915,181.113,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.003,181.544,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.072,181.987,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.125,182.439,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.161,182.9,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.181,183.365,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.188,183.834,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.181,184.303,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.161,184.77,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.13,185.233,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.088,185.69,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.036,186.137,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.974,186.574,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.904,186.997,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1691\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":33,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[44.84,258.096,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.844,257.861,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.853,257.618,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.867,257.371,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.886,257.12,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.911,256.868,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.943,256.618,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.983,256.371,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.03,256.13,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.085,255.897,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.149,255.675,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.222,255.465,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.305,255.271,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.392,255.089,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.442,254.877,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.45,254.633,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.421,254.362,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.357,254.068,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.262,253.756,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.139,253.431,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.991,253.098,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.821,252.759,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.632,252.419,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.426,252.081,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.207,251.749,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.976,251.426,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.736,251.114,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.488,250.816,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.235,250.534,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.978,250.271,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.719,250.027,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.458,249.806,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.196,249.607,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.935,249.432,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.675,249.283,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.415,249.158,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.155,249.06,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.897,248.987,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.637,248.94,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.376,248.918,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.112,248.92,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.844,248.946,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.57,248.994,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.288,249.064,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.012,249.162,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.745,249.288,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.488,249.442,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.243,249.622,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.011,249.827,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.794,250.054,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.592,250.304,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.407,250.573,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.24,250.86,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.093,251.164,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.965,251.483,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.858,251.813,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.772,252.155,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.709,252.505,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.669,252.862,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.653,253.223,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.661,253.586,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.692,253.951,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.749,254.313,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.83,254.672,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.936,255.026,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.067,255.372,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.222,255.709,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.401,256.035,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.604,256.349,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.83,256.648,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.077,256.932,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.347,257.198,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.636,257.446,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.944,257.675,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.264,257.889,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.594,258.088,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.932,258.269,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.279,258.428,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.633,258.565,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.993,258.676,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.359,258.762,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.73,258.821,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.105,258.852,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.485,258.856,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.869,258.832,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.256,258.781,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.647,258.703,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.041,258.6,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.438,258.472,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.839,258.322,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"576\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":34,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[33.652,139.713,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.981,139.258,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.296,138.83,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.601,138.43,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.899,138.059,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.192,137.719,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.484,137.411,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.776,137.134,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.071,136.888,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.372,136.673,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.68,136.489,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.997,136.335,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.326,136.208,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.667,136.109,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.024,136.04,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.397,136.005,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.788,136.003,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.2,136.035,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.634,136.098,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.092,136.193,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.575,136.319,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.086,136.475,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.625,136.659,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.194,136.87,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.794,137.107,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.428,137.368,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.096,137.651,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.799,137.954,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.538,138.276,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.315,138.615,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.131,138.968,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.986,139.334,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.882,139.71,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.818,140.095,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.796,140.485,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.817,140.879,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.879,141.275,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.985,141.67,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.134,142.062,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.325,142.449,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.561,142.829,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.839,143.2,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.16,143.559,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.523,143.906,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.921,144.262,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.346,144.636,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.794,145.02,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.264,145.409,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.753,145.798,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.257,146.183,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.775,146.56,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.304,146.924,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.842,147.272,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.388,147.601,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.939,147.908,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.493,148.19,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.051,148.447,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.609,148.676,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.168,148.875,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.726,149.044,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.282,149.183,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[27.835,149.29,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.385,149.367,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.931,149.412,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.473,149.427,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.011,149.412,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.545,149.369,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.073,149.3,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.597,149.205,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.116,149.086,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.631,148.947,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.142,148.789,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.648,148.615,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.151,148.427,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.612,148.198,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.011,147.915,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.349,147.581,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.625,147.201,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.839,146.777,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.99,146.314,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.079,145.815,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.105,145.284,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.07,144.725,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.974,144.142,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.817,143.539,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.599,142.919,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.323,142.287,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.989,141.647,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.599,141.002,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.152,140.356,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.652,139.713,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1692\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":35,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[4.962,144.774,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.736,144.983,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.552,145.225,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.412,145.497,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.316,145.797,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.266,146.125,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.263,146.477,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.307,146.852,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.4,147.248,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.542,147.662,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.733,148.092,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.976,148.536,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[5.269,148.991,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[5.606,149.476,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[5.975,150.012,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[6.371,150.592,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[6.792,151.211,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[7.232,151.862,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[7.691,152.539,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[8.163,153.236,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[8.647,153.949,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[9.14,154.671,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[9.64,155.399,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.144,156.128,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.65,156.853,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.158,157.571,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.664,158.278,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[12.169,158.97,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[12.671,159.644,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[13.168,160.298,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[13.66,160.928,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[14.147,161.532,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[14.627,162.107,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.101,162.652,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.568,163.164,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.029,163.642,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.483,164.084,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.931,164.489,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.373,164.856,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.809,165.183,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.242,165.471,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.67,165.718,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.095,165.925,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.504,166.081,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.879,166.179,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.217,166.218,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.518,166.199,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.782,166.124,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.009,165.993,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.197,165.81,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.346,165.575,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.457,165.292,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.529,164.961,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.563,164.585,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.558,164.167,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.515,163.71,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.435,163.215,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.317,162.687,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.163,162.128,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.973,161.541,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.748,160.929,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.489,160.295,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.197,159.642,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.873,158.974,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.519,158.293,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.135,157.603,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.722,156.907,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.283,156.208,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.818,155.508,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.33,154.812,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.82,154.122,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.289,153.44,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.739,152.77,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.174,152.113,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[14.596,151.474,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[14.006,150.853,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[13.406,150.253,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[12.797,149.675,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[12.182,149.121,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.561,148.592,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.937,148.09,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.311,147.617,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[9.686,147.172,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[9.063,146.759,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[8.445,146.376,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[7.833,146.026,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[7.231,145.708,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[6.64,145.424,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[6.063,145.173,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[5.503,144.957,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.962,144.774,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"290\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":36,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[195.36,9.184,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.865,10.085,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[196.361,10.989,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[196.847,11.893,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[197.324,12.792,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[197.791,13.684,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[198.248,14.567,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[198.695,15.435,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.131,16.287,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.558,17.118,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.976,17.926,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[200.385,18.706,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[200.783,19.456,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[201.163,20.172,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[201.524,20.852,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[201.864,21.492,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.182,22.092,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.477,22.65,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.748,23.163,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.994,23.631,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.213,24.052,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.407,24.426,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.573,24.752,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.713,25.028,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.824,25.256,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.907,25.434,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.963,25.564,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.99,25.644,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.961,25.658,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.906,25.593,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.823,25.482,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.714,25.325,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.578,25.123,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.417,24.878,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.232,24.591,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.023,24.263,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.791,23.896,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.537,23.491,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.263,23.051,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[201.969,22.577,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[201.656,22.071,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[201.329,21.536,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[200.993,20.975,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[200.65,20.391,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[200.301,19.788,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.947,19.168,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.587,18.535,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.224,17.892,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[198.857,17.242,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[198.489,16.588,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[198.12,15.933,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[197.751,15.279,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[197.384,14.629,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[197.02,13.986,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[196.66,13.353,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[196.306,12.731,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.959,12.123,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.621,11.531,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.295,10.958,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.981,10.405,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.681,9.874,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.399,9.367,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.136,8.885,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.894,8.43,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.677,8.004,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.486,7.608,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.324,7.242,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.194,6.907,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.1,6.605,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.044,6.337,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.029,6.101,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.053,5.901,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.096,5.744,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.156,5.629,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.231,5.556,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.32,5.526,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.423,5.537,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.536,5.59,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.66,5.685,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.793,5.819,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.934,5.993,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.082,6.206,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.234,6.457,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.391,6.745,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.551,7.069,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.713,7.428,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.876,7.82,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.039,8.244,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.201,8.699,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.36,9.184,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"11\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":37,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[136.956,36.045,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.293,36.5,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.606,36.915,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.893,37.29,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.155,37.623,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.39,37.913,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.597,38.161,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.777,38.365,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.927,38.524,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.049,38.64,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.141,38.71,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.206,38.733,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.265,38.694,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.321,38.591,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.373,38.43,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.423,38.215,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.467,37.95,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.508,37.638,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.543,37.285,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.573,36.893,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.597,36.468,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.616,36.012,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.629,35.53,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.637,35.024,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.639,34.5,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.636,33.959,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.628,33.405,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.615,32.842,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.599,32.273,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.579,31.7,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.556,31.126,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.531,30.554,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.504,29.986,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.476,29.425,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.449,28.873,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.423,28.332,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.399,27.804,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.378,27.291,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.362,26.795,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.352,26.316,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.348,25.856,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.352,25.416,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.352,24.996,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.346,24.596,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.333,24.218,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.314,23.863,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.289,23.534,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.258,23.23,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.221,22.954,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.179,22.707,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.132,22.49,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.079,22.303,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[139.022,22.148,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.96,22.025,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.895,21.935,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.825,21.878,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.752,21.855,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.676,21.867,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.598,21.913,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.517,21.993,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.434,22.109,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.35,22.259,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.264,22.442,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.178,22.66,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.092,22.911,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[138.005,23.195,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.92,23.511,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.835,23.858,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.751,24.235,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.67,24.64,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.59,25.073,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.514,25.531,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.444,26.003,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.382,26.485,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.327,26.979,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.276,27.482,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.231,27.997,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.191,28.521,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.154,29.055,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.122,29.599,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.092,30.151,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.066,30.712,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.043,31.281,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.023,31.858,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[137.005,32.442,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.99,33.032,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.978,33.627,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.969,34.227,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.962,34.831,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.958,35.437,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[136.956,36.045,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1694\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":38,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[233.291,37.866,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[233.526,37.31,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[233.746,36.738,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[233.951,36.152,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.141,35.555,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.314,34.949,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.473,34.338,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.615,33.724,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.743,33.11,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.854,32.497,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.951,31.889,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[235.031,31.286,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[235.093,30.688,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[235.137,30.098,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[235.165,29.516,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[235.174,28.946,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[235.167,28.388,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[235.143,27.846,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[235.103,27.319,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[235.047,26.812,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.976,26.324,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.891,25.859,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.793,25.418,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.682,25.003,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.559,24.616,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.425,24.258,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.281,23.931,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[234.128,23.637,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[233.968,23.377,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[233.801,23.154,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[233.628,22.967,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[233.451,22.819,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[233.271,22.711,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[233.088,22.645,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[232.905,22.62,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[232.721,22.639,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[232.539,22.702,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[232.359,22.81,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[232.182,22.963,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[232.009,23.163,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.842,23.408,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.693,23.675,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.566,23.95,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.46,24.231,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.372,24.52,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.3,24.817,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.242,25.121,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.197,25.433,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.163,25.752,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.138,26.08,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.121,26.416,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.111,26.76,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.107,27.113,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.107,27.474,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.111,27.845,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.117,28.224,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.124,28.612,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.132,29.009,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.139,29.416,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.145,29.831,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.149,30.255,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.149,30.688,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.145,31.13,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.136,31.579,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.12,32.037,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.097,32.502,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.065,32.974,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.023,33.452,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.97,33.936,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.904,34.425,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.824,34.917,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.75,35.391,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.694,35.835,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.657,36.247,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.64,36.626,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.642,36.973,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.665,37.286,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.71,37.564,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.775,37.807,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.862,38.014,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[230.971,38.186,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.102,38.32,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.256,38.419,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.432,38.48,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.631,38.504,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[231.852,38.49,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[232.096,38.44,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[232.362,38.352,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[232.65,38.227,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[232.96,38.065,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[233.291,37.866,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"14\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":39,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[183.264,47.5,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.105,46.994,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.943,46.521,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.779,46.082,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.615,45.683,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.452,45.326,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.293,45.016,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.137,44.757,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.988,44.551,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.845,44.403,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.721,44.297,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.631,44.203,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.57,44.121,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.537,44.052,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.528,43.995,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.54,43.951,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.571,43.921,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.681,43.902,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.84,43.942,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.933,43.984,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.032,44.043,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.137,44.118,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.245,44.209,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.355,44.318,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.465,44.444,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.575,44.588,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.683,44.75,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.788,44.931,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.888,45.131,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.982,45.35,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.07,45.589,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.15,45.848,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.22,46.127,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.281,46.426,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.33,46.745,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.366,47.085,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.388,47.445,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.395,47.826,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.391,48.223,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.383,48.63,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.371,49.044,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.356,49.464,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.337,49.887,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.315,50.312,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.291,50.736,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.263,51.157,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.234,51.573,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.202,51.982,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.169,52.382,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.135,52.77,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.099,53.145,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.063,53.504,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.026,53.846,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.989,54.168,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.953,54.469,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.917,54.747,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.883,54.999,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.85,55.225,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.818,55.423,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.789,55.592,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.762,55.73,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.738,55.836,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.716,55.909,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.672,55.922,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.664,55.856,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.661,55.754,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.665,55.621,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.682,55.466,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.71,55.284,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.747,55.076,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.791,54.839,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.84,54.572,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.892,54.275,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.945,53.948,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.999,53.591,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.051,53.204,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.101,52.788,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.147,52.345,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.188,51.875,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.223,51.381,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.252,50.866,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.274,50.332,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.288,49.781,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.294,49.219,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.292,48.648,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.282,48.074,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.264,47.5,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"297\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":40,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[91.824,41.976,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.815,42.04,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.815,42.122,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.824,42.222,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.841,42.341,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.867,42.479,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.901,42.637,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.943,42.813,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.993,43.009,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.049,43.224,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.114,43.458,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.185,43.711,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.264,43.98,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.35,44.267,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.442,44.567,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.541,44.882,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.644,45.208,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.752,45.545,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.864,45.89,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.98,46.241,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.098,46.597,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.218,46.954,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.339,47.312,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.461,47.667,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.582,48.017,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.703,48.361,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.821,48.694,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.936,49.016,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.047,49.324,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.154,49.615,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.256,49.887,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.351,50.137,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.44,50.364,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.521,50.566,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.594,50.739,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.658,50.883,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.712,50.995,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.756,51.074,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.789,51.118,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.858,51.138,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.911,51.137,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.973,51.124,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.04,51.097,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.108,51.053,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.176,50.99,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.241,50.906,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.3,50.799,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.352,50.668,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.395,50.511,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.427,50.329,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.447,50.121,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.455,49.886,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.45,49.626,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.431,49.34,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.398,49.031,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.351,48.699,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.291,48.346,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.219,47.975,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.134,47.589,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.038,47.189,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.932,46.78,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.819,46.365,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.698,45.949,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.574,45.537,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.447,45.133,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.32,44.742,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.196,44.372,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.077,44.027,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.964,43.711,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.845,43.409,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.721,43.12,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.593,42.847,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.462,42.59,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.328,42.351,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.194,42.132,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.06,41.934,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.927,41.758,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.797,41.606,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.67,41.478,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.548,41.377,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.432,41.302,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.323,41.255,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.222,41.237,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.131,41.249,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.049,41.292,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.979,41.365,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.92,41.47,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.874,41.606,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.842,41.775,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.824,41.976,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1695\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":41,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[41.366,588.678,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.622,588.687,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.9,588.668,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.198,588.62,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.514,588.542,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.845,588.433,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.191,588.29,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.548,588.112,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.916,587.901,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.301,587.696,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.7,587.505,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.109,587.325,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.522,587.153,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.935,586.986,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.342,586.822,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.741,586.659,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.128,586.494,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.499,586.326,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.851,586.152,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.182,585.97,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.489,585.781,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.77,585.582,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.024,585.371,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.25,585.149,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.445,584.915,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.61,584.668,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.745,584.408,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.848,584.135,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.919,583.849,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.961,583.551,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.972,583.241,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.953,582.921,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.907,582.591,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.834,582.253,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.736,581.909,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.614,581.561,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.472,581.21,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.311,580.861,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[49.132,580.514,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.923,580.177,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.68,579.85,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.406,579.536,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[48.103,579.234,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.773,578.948,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.417,578.677,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[47.04,578.423,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.642,578.187,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[46.227,577.969,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.798,577.772,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.358,577.596,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.908,577.442,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.452,577.31,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.993,577.201,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.534,577.117,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.077,577.058,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.625,577.024,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.181,577.016,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.749,577.035,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.329,577.08,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.925,577.152,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.54,577.251,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.175,577.377,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.834,577.53,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.517,577.711,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.227,577.918,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.965,578.151,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.733,578.41,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.533,578.694,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.364,579.003,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.221,579.334,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.103,579.685,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.009,580.057,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.942,580.446,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.901,580.853,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.887,581.276,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.9,581.713,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.94,582.163,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.009,582.624,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.105,583.094,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.229,583.572,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.381,584.056,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.561,584.542,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.768,585.03,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.003,585.517,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.264,586,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.552,586.477,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.866,586.945,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.205,587.403,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.568,587.846,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.956,588.272,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.366,588.678,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1687\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":42,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[45.598,495.423,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.517,494.948,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.421,494.475,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.309,494.006,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.182,493.54,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.04,493.077,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.883,492.618,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.711,492.162,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.524,491.714,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.323,491.276,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.107,490.852,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.878,490.442,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.635,490.049,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.381,489.673,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.115,489.317,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.839,488.981,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.554,488.667,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.262,488.375,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.963,488.107,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.659,487.864,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.352,487.645,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.044,487.452,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.735,487.285,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.427,487.145,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.123,487.031,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.824,486.944,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.531,486.884,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.247,486.85,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.973,486.844,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.712,486.863,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.464,486.909,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.232,486.98,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.017,487.077,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.821,487.197,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.646,487.341,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.494,487.508,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.366,487.697,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.263,487.907,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.18,488.145,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.113,488.417,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.062,488.72,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.026,489.051,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.005,489.407,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.999,489.785,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.008,490.184,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.032,490.599,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.069,491.029,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.122,491.471,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.189,491.92,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.27,492.376,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.365,492.834,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.475,493.292,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.599,493.746,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.738,494.194,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.891,494.633,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.058,495.059,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.241,495.469,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.438,495.861,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.65,496.23,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.878,496.574,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.12,496.89,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.378,497.174,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.652,497.423,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.942,497.634,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.248,497.803,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.57,497.928,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.909,498.004,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.265,498.029,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.626,498.025,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.985,498.009,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.338,497.982,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.684,497.944,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.02,497.895,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.346,497.835,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.658,497.764,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.955,497.682,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.235,497.589,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.497,497.487,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.738,497.374,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.958,497.252,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.154,497.121,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.326,496.981,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.472,496.834,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.591,496.678,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.682,496.515,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.744,496.346,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.778,495.99,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.749,495.805,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.689,495.616,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.598,495.423,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1274\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":43,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[512.488,7.664,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[512.949,7.465,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[513.386,7.304,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[513.796,7.18,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[514.177,7.093,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[514.527,7.04,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[514.843,7.022,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[515.139,7.055,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[515.443,7.166,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[515.751,7.352,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.061,7.608,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.37,7.929,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.673,8.31,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.969,8.747,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.256,9.234,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.531,9.766,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.791,10.34,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.036,10.949,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.264,11.59,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.473,12.257,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.662,12.946,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.831,13.653,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.978,14.372,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.104,15.098,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.208,15.828,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.289,16.556,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.349,17.279,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.386,17.99,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.403,18.686,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.399,19.363,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.376,20.015,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.334,20.639,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.275,21.229,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.2,21.782,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.111,22.292,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[519.01,22.756,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.898,23.17,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.771,23.536,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.618,23.867,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.44,24.163,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.238,24.423,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[518.013,24.646,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.767,24.832,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.502,24.981,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[517.219,25.091,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.92,25.164,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.607,25.199,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[516.282,25.197,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[515.947,25.158,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[515.604,25.081,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[515.255,24.969,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[514.902,24.821,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[514.548,24.638,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[514.193,24.422,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[513.841,24.172,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[513.493,23.891,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[513.151,23.578,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[512.818,23.237,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[512.495,22.867,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[512.184,22.471,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[511.887,22.05,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[511.605,21.605,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[511.341,21.138,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[511.095,20.651,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[510.87,20.145,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[510.666,19.624,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[510.484,19.087,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[510.319,18.543,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[510.161,18,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[510.013,17.459,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.878,16.923,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.761,16.391,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.664,15.865,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.589,15.346,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.538,14.833,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.513,14.328,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.516,13.831,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.548,13.341,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.61,12.86,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.701,12.387,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.823,11.923,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[509.974,11.467,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[510.155,11.018,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[510.364,10.577,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[510.6,10.144,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[510.863,9.717,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[511.149,9.296,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[511.457,8.882,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[511.785,8.472,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[512.129,8.066,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[512.488,7.664,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"33\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":44,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[503.811,23.46,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.721,24.135,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.581,24.81,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.392,25.479,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.155,26.14,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.872,26.787,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.545,27.416,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.173,28.028,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.757,28.62,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.3,29.19,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.803,29.737,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.269,30.259,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.701,30.754,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.1,31.221,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.471,31.658,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.816,32.065,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.14,32.44,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.446,32.781,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.737,33.089,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.017,33.363,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[494.292,33.601,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[493.564,33.803,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.839,33.969,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.119,34.099,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.411,34.191,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.717,34.247,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.043,34.265,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.392,34.246,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.769,34.191,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.178,34.098,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.623,33.97,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.107,33.805,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[486.636,33.605,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[486.211,33.371,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[485.837,33.102,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[485.518,32.801,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[485.242,32.47,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[484.956,32.125,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[484.66,31.769,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[484.361,31.404,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[484.066,31.031,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[483.782,30.652,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[483.515,30.267,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[483.269,29.879,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[483.05,29.488,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[482.861,29.096,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[482.705,28.703,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[482.587,28.311,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[482.509,27.921,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[482.471,27.534,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[482.476,27.15,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[482.525,26.771,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[482.616,26.397,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[482.751,26.029,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[482.928,25.668,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[483.145,25.314,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[483.4,24.969,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[483.691,24.632,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[484.014,24.305,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[484.366,23.987,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[484.741,23.679,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[485.135,23.382,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[485.542,23.096,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[485.956,22.821,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[486.37,22.558,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[486.776,22.306,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.179,22.068,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[487.629,21.853,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.13,21.664,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[488.679,21.501,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.27,21.362,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.901,21.249,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.568,21.161,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.266,21.097,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.991,21.059,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.739,21.044,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[493.506,21.054,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[494.287,21.088,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.078,21.145,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.874,21.225,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.672,21.327,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.466,21.451,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.253,21.597,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.029,21.764,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.789,21.951,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.529,22.157,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.246,22.383,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.936,22.626,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[502.596,22.888,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.222,23.166,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[503.811,23.46,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"32\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":45,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[588.635,32.882,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.072,33.246,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[587.531,33.578,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[587.017,33.878,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.535,34.144,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.083,34.376,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.596,34.573,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.058,34.736,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[584.477,34.866,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[583.863,34.963,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[583.222,35.027,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[582.561,35.06,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[581.889,35.062,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[581.21,35.034,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[580.532,34.977,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[579.86,34.893,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[579.198,34.782,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[578.553,34.646,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[577.927,34.486,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[577.325,34.305,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[576.75,34.102,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[576.205,33.881,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[575.693,33.642,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[575.215,33.387,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.773,33.119,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.368,32.839,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.001,32.548,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.671,32.25,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.378,31.945,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.122,31.636,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.9,31.325,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.711,31.014,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.553,30.705,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.423,30.399,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.316,30.099,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.232,29.807,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.192,29.513,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.204,29.217,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.268,28.918,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.38,28.619,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.542,28.321,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.75,28.026,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.003,27.734,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.298,27.449,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.634,27.17,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.008,26.9,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.418,26.639,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.861,26.39,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[575.334,26.154,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[575.834,25.932,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[576.358,25.726,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[576.904,25.536,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[577.468,25.364,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[578.047,25.211,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[578.638,25.079,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[579.238,24.967,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[579.843,24.878,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[580.451,24.812,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[581.058,24.769,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[581.661,24.751,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[582.258,24.757,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[582.845,24.789,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[583.419,24.847,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[583.978,24.93,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[584.519,25.038,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.04,25.172,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[585.544,25.317,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.029,25.469,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.491,25.632,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[586.928,25.807,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[587.336,25.994,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[587.715,26.195,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.061,26.411,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.373,26.643,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.65,26.891,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.89,27.156,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.095,27.439,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.262,27.738,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.393,28.054,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.488,28.387,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.547,28.736,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.573,29.101,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.566,29.48,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.528,29.873,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.462,30.278,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.37,30.695,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.255,31.12,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[589.12,31.554,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.969,31.994,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.806,32.437,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[588.635,32.882,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"414\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":46,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[640.884,150.101,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.371,149.48,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.88,148.84,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.411,148.187,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.965,147.524,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.541,146.854,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.144,146.176,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.773,145.493,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.431,144.808,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.117,144.123,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.834,143.44,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.58,142.762,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.358,142.091,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.166,141.43,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.006,140.781,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.877,140.147,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.778,139.53,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.711,138.934,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.674,138.36,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.667,137.812,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.689,137.291,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.739,136.801,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.818,136.345,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.923,135.923,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.054,135.54,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.21,135.197,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.39,134.896,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.592,134.64,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.816,134.431,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.059,134.27,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.322,134.16,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.898,134.098,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.208,134.148,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.532,134.256,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.871,134.395,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.225,134.548,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.591,134.716,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.968,134.9,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.352,135.103,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.74,135.324,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.131,135.566,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.522,135.829,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.912,136.113,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.297,136.419,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.678,136.748,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.051,137.098,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.415,137.471,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.77,137.866,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.114,138.281,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.445,138.718,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.764,139.174,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.069,139.648,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.361,140.14,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.638,140.648,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.9,141.17,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.148,141.704,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.381,142.249,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.601,142.802,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.807,143.362,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.001,143.924,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.182,144.487,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.354,145.048,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.516,145.603,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.67,146.15,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.8,146.686,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.893,147.208,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.948,147.714,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.967,148.202,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.949,148.668,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.895,149.111,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.806,149.529,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.682,149.918,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.525,150.276,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.335,150.602,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.113,150.894,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.861,151.148,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.579,151.365,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.27,151.541,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.933,151.675,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.571,151.765,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.185,151.812,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.776,151.812,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.346,151.766,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.897,151.673,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.43,151.532,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.946,151.342,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.448,151.104,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.937,150.818,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.415,150.483,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.884,150.101,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"369\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":47,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[610.865,248.89,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.769,248.619,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.704,248.385,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.672,248.188,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.671,248.025,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.703,247.886,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.856,247.678,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.294,247.55,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.706,247.586,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.202,247.718,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.768,247.94,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.074,248.083,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.392,248.247,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.723,248.43,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.064,248.631,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.413,248.848,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.77,249.082,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.133,249.329,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.5,249.589,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.871,249.86,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.243,250.141,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.616,250.429,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.989,250.723,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.361,251.021,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.73,251.321,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.095,251.622,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.456,251.921,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.812,252.216,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.162,252.508,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.502,252.804,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.831,253.103,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.149,253.403,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.454,253.702,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.744,254,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.018,254.294,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.276,254.583,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.516,254.867,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.737,255.143,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.939,255.41,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.119,255.666,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.278,255.911,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.415,256.142,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.528,256.359,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.617,256.56,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.682,256.743,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.722,256.909,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.736,257.055,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.687,257.284,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.102,257.455,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.686,257.349,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.171,257.139,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.875,257.001,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.553,256.851,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.206,256.688,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.84,256.511,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.456,256.32,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.057,256.114,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.647,255.892,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.228,255.655,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.802,255.4,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.372,255.13,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.938,254.844,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.505,254.542,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.072,254.225,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.642,253.893,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.216,253.548,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.796,253.19,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.382,252.821,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.977,252.443,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.581,252.055,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.195,251.661,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.821,251.262,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.459,250.86,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.11,250.458,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.776,250.057,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.456,249.66,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.152,249.27,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[610.865,248.89,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"410\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":48,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[617.679,171.492,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.018,171.893,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.352,172.284,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.677,172.663,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.995,173.031,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.303,173.386,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.6,173.727,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.886,174.055,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.159,174.368,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.418,174.665,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.662,174.946,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.892,175.21,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.105,175.457,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.301,175.686,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.48,175.896,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.642,176.087,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.785,176.258,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.909,176.409,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.014,176.539,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.099,176.648,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.211,176.8,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.069,176.708,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.976,176.609,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.865,176.486,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.735,176.339,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.586,176.168,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.42,175.972,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.236,175.757,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.028,175.556,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.797,175.371,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.548,175.203,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.284,175.048,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.007,174.907,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.72,174.778,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.427,174.658,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.128,174.548,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.828,174.444,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.527,174.347,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.228,174.255,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.932,174.165,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.642,174.077,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.36,173.989,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.085,173.9,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.821,173.807,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.569,173.71,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.329,173.607,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.104,173.495,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.893,173.375,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.699,173.243,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.522,173.098,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.364,172.939,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.224,172.764,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.105,172.57,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.007,172.357,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.931,172.122,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.878,171.864,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.849,171.58,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.843,171.276,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.857,170.987,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.888,170.718,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.935,170.47,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.997,170.243,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.072,170.039,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.16,169.856,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.26,169.696,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.369,169.56,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.487,169.446,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.743,169.29,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.301,169.266,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.582,169.4,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.719,169.504,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.852,169.631,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.981,169.783,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.103,169.958,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.219,170.157,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.328,170.379,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.429,170.624,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.521,170.892,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.605,171.181,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.679,171.492,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"368\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":49,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[613.335,474.276,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.113,473.795,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.912,473.24,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.728,472.662,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.559,472.08,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.406,471.496,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.27,470.912,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.15,470.328,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.049,469.746,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.966,469.166,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.901,468.589,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.856,468.017,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.83,467.448,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.824,466.885,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.837,466.327,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.871,465.775,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.925,465.229,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[611.999,464.69,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.093,464.156,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.207,463.628,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.342,463.106,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.497,462.59,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.672,462.078,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[612.867,461.572,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.082,461.069,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.316,460.57,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.571,460.073,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.844,459.578,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.138,459.084,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.45,458.59,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.782,458.093,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.132,457.594,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.502,457.092,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.887,456.611,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.285,456.164,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.695,455.75,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.113,455.372,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.538,455.031,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.967,454.727,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.4,454.461,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.832,454.234,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.262,454.047,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.687,453.9,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.106,453.792,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.516,453.725,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.915,453.699,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.299,453.712,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.668,453.766,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.019,453.86,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.35,453.993,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.942,454.375,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.2,454.623,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.43,454.907,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.63,455.226,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.799,455.581,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.936,455.968,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.038,456.388,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.106,456.839,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.138,457.318,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.133,457.825,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.09,458.358,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.011,458.916,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.908,459.511,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.785,460.143,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.643,460.809,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.479,461.504,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.293,462.222,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.084,462.959,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.853,463.711,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.598,464.472,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.32,465.237,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.018,466.003,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.691,466.763,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.342,467.514,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.969,468.251,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.573,468.968,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.155,469.662,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.715,470.327,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.255,470.958,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.776,471.55,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.279,472.1,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.766,472.601,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.238,473.049,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.698,473.439,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.147,473.765,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.589,474.024,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.026,474.209,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.46,474.317,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.895,474.341,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1437\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":50,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[489.552,647.715,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.759,646.978,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.983,646.251,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.224,645.54,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.482,644.846,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.756,644.173,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.046,643.523,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.349,642.899,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.665,642.303,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.993,641.737,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.33,641.203,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.676,640.703,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[493.028,640.238,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[493.384,639.811,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[493.742,639.421,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[494.101,639.07,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[494.458,638.76,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[494.81,638.49,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.156,638.262,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.492,638.075,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.818,637.931,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.129,637.829,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.424,637.768,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.701,637.75,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.188,637.837,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.573,638.086,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.838,638.489,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.966,639.038,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.99,639.37,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.007,639.748,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.016,640.167,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.017,640.625,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.009,641.118,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.992,641.644,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.965,642.198,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.927,642.777,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.878,643.379,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.818,643.998,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.745,644.632,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.66,645.277,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.562,645.93,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.451,646.586,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.327,647.241,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.19,647.892,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.04,648.535,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.878,649.165,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.703,649.779,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.516,650.373,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.319,650.942,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.112,651.482,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.897,651.988,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.674,652.458,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.446,652.885,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[495.214,653.265,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[494.749,653.869,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[494.298,654.233,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[493.862,654.408,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[493.427,654.475,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.996,654.432,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.573,654.282,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[492.161,654.029,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.764,653.676,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.385,653.229,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[491.029,652.696,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.699,652.082,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.546,651.748,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.4,651.398,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.264,651.032,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.136,650.652,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[490.019,650.259,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.912,649.855,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.815,649.441,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.731,649.018,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.658,648.589,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[489.599,648.154,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1591\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":51,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[635.656,498.061,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.015,497.919,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.403,497.772,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.819,497.622,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.258,497.468,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.718,497.314,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.196,497.159,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.689,497.006,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.193,496.854,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.707,496.704,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.228,496.558,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.752,496.415,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.278,496.277,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.804,496.142,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.325,496.012,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.842,495.886,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.351,495.765,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.851,495.649,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.34,495.537,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.816,495.428,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.278,495.324,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.724,495.222,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.154,495.123,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.567,495.027,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.96,494.932,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.335,494.838,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.691,494.744,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.027,494.649,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.636,494.458,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.126,494.292,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.766,494.035,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.291,494.059,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.646,494.199,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.09,494.331,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.446,494.492,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.094,494.583,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.725,494.679,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.339,494.781,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.939,494.889,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.526,495.001,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.102,495.118,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.669,495.238,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.228,495.362,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.785,495.488,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.354,495.611,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.935,495.732,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.527,495.85,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.13,495.966,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.744,496.081,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.369,496.194,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.004,496.306,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.65,496.417,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.305,496.527,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.971,496.636,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.331,496.85,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.729,497.06,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.162,497.265,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.629,497.464,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.125,497.654,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.645,497.834,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.182,497.999,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.729,498.145,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.273,498.27,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1303\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":52,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[631.205,586.111,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.568,585.803,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.136,585.243,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.49,584.763,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.622,584.203,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.092,583.772,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.553,583.696,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.223,583.693,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.855,583.712,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.453,583.754,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.019,583.818,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.556,583.904,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.068,584.011,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.558,584.14,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.028,584.288,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.482,584.456,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.925,584.643,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.358,584.848,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.785,585.07,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.211,585.309,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.638,585.563,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.072,585.83,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.53,586.099,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.015,586.369,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.526,586.638,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.063,586.906,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.626,587.174,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.215,587.441,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.829,587.706,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.469,587.97,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.134,588.233,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.537,588.75,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.036,589.255,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.629,589.744,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.31,590.213,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.073,590.656,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.91,591.07,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.786,591.619,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.775,592.184,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.294,592.471,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.781,592.376,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.079,592.294,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.41,592.192,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.772,592.069,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.163,591.926,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.581,591.765,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.024,591.585,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.49,591.389,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.976,591.177,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.48,590.95,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623,590.709,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.534,590.455,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.079,590.189,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.635,589.912,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.197,589.625,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.765,589.329,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.335,589.026,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.907,588.716,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.479,588.399,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.047,588.079,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.612,587.754,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.17,587.426,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.72,587.097,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.262,586.766,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.419607847929,0.611764729023,0.407843142748,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1598\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":53,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[565.745,638.828,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[565.047,639.051,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[564.394,639.274,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[563.785,639.495,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[563.219,639.715,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.696,639.935,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.215,640.152,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.775,640.369,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.376,640.583,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.017,640.796,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[560.697,641.007,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[560.172,641.422,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[559.652,642.024,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[559.423,642.592,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[559.44,643.114,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[559.742,643.708,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[560.205,644.136,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[560.715,644.306,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.197,644.34,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.475,644.333,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[561.777,644.312,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.099,644.276,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.44,644.226,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[562.798,644.162,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[563.17,644.085,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[563.554,643.996,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[563.948,643.894,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[564.35,643.781,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[564.757,643.657,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[565.167,643.524,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[565.576,643.382,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[565.984,643.231,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[566.386,643.074,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[566.782,642.909,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[567.168,642.739,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[567.542,642.564,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[567.903,642.385,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[568.247,642.203,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[568.573,642.02,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[568.878,641.834,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.421,641.464,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.862,641.098,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.413,640.579,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.938,639.947,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[571.101,639.389,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.665,638.836,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.281,638.674,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.781,638.553,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.491,638.51,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.177,638.479,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[568.841,638.461,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[568.484,638.456,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[568.111,638.465,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[567.725,638.489,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[567.329,638.528,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[566.928,638.581,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[566.525,638.65,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[566.127,638.735,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":18,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1554\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":54,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[187.525,654.877,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.382,654.303,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.384,653.699,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.499,653.063,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.33,653.088,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.208,653.376,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.063,653.7,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.894,654.044,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.8,654.218,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.701,654.392,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.595,654.564,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.483,654.734,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.365,654.899,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.24,655.059,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.11,655.214,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.974,655.363,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.833,655.505,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.686,655.641,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.534,655.769,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.378,655.889,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.218,656.002,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.055,656.107,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.889,656.204,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.721,656.293,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.553,656.374,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.384,656.448,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.218,656.513,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.06,656.568,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.92,656.609,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.799,656.637,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.696,656.652,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.545,656.646,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.691,656.269,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.792,656.197,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.909,656.122,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.044,656.046,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.195,655.969,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.362,655.892,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.544,655.816,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.741,655.741,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.953,655.669,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.178,655.6,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.416,655.535,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.666,655.473,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.927,655.416,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.199,655.364,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.481,655.317,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.771,655.275,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.069,655.239,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.373,655.207,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.684,655.181,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1681\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":55,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[114.044,628.654,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[113.896,628.947,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[113.714,629.277,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[113.501,629.638,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[113.261,630.027,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[112.996,630.437,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[112.711,630.864,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[112.406,631.305,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[112.086,631.754,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[111.754,632.209,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[111.411,632.665,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[111.06,633.12,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[110.703,633.571,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[110.344,634.014,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[109.982,634.448,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[109.622,634.87,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[109.264,635.278,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[108.909,635.67,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[108.56,636.044,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[108.218,636.399,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[107.884,636.734,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[107.558,637.046,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[107.242,637.335,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[106.937,637.6,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[106.642,637.839,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[106.358,638.052,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[106.086,638.237,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[105.825,638.395,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[105.577,638.523,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[105.344,638.621,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[105.129,638.687,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.931,638.722,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.751,638.726,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.59,638.701,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.447,638.646,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.324,638.562,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.22,638.45,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.136,638.311,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.072,638.147,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[103.998,637.512,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.047,636.984,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.101,636.693,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.173,636.386,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.265,636.066,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.374,635.733,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.501,635.39,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.645,635.038,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.806,634.679,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[104.984,634.314,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[105.176,633.947,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[105.384,633.577,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[105.606,633.208,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[105.841,632.841,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[106.088,632.477,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[106.347,632.119,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[106.613,631.769,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[106.869,631.435,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[107.117,631.114,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[107.355,630.808,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[107.586,630.516,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[107.808,630.237,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[108.023,629.97,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[108.232,629.715,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[108.435,629.473,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[108.632,629.242,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[108.825,629.023,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[109.015,628.816,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[109.202,628.622,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[109.388,628.439,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[109.573,628.269,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[109.758,628.113,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[109.945,627.97,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[110.135,627.842,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[110.33,627.729,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[110.529,627.633,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[110.735,627.554,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[110.949,627.494,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[111.173,627.455,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[111.407,627.439,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[111.654,627.446,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[111.915,627.479,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[112.192,627.54,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[112.485,627.632,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[112.798,627.757,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[113.131,627.917,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[113.472,628.119,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[113.777,628.365,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[114.044,628.654,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1683\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":56,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[214.889,632.132,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[214.55,632.487,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[214.231,632.815,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.935,633.115,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.66,633.386,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.408,633.628,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.178,633.839,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.972,634.02,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.789,634.169,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.63,634.287,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.496,634.372,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.299,634.448,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.295,633.924,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.487,633.51,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.619,633.262,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.775,632.988,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[212.953,632.689,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.155,632.366,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.379,632.02,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.625,631.652,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[213.892,631.267,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[214.169,630.885,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[214.451,630.511,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[214.735,630.145,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[215.021,629.786,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[215.306,629.437,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[215.588,629.096,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[215.866,628.765,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.139,628.443,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.406,628.13,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.666,627.828,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.918,627.537,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.162,627.256,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.397,626.986,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.624,626.727,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.843,626.481,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.054,626.248,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.257,626.027,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.453,625.821,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.644,625.629,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.829,625.452,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.011,625.292,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.19,625.15,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.369,625.027,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.548,624.924,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.73,624.842,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.916,624.784,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.109,624.751,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.311,624.745,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.525,624.768,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.749,624.823,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.949,624.908,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[221.12,625.022,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[221.26,625.162,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[221.452,625.516,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[221.527,625.951,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[221.49,626.452,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[221.347,627,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[221.237,627.287,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[221.103,627.58,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.946,627.876,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.766,628.173,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.564,628.471,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.343,628.766,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[220.101,629.058,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.842,629.344,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.565,629.622,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[219.272,629.892,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.963,630.152,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.641,630.401,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[218.306,630.638,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.959,630.862,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.602,631.072,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[217.234,631.267,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.859,631.448,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.476,631.614,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[216.086,631.765,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[215.691,631.903,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[215.292,632.026,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[214.889,632.132,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1675\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":57,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[258.984,627.393,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[259.244,627.105,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[259.504,626.813,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[259.762,626.519,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[260.015,626.225,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[260.263,625.934,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[260.502,625.647,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[260.733,625.367,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[260.952,625.095,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.16,624.831,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.356,624.579,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.538,624.337,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.707,624.108,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.861,623.893,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[262.001,623.691,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[262.239,623.331,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[262.496,622.907,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[262.737,622.51,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[262.529,623.079,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[262.342,623.419,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[262.109,623.823,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.977,624.047,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.836,624.282,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.687,624.529,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.53,624.785,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.367,625.049,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.199,625.32,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[261.027,625.595,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[260.85,625.874,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[260.671,626.156,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[260.491,626.437,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[260.31,626.718,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[260.13,626.995,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[259.951,627.269,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[259.774,627.536,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[259.601,627.797,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[259.432,628.049,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[259.267,628.291,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[259.109,628.522,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.958,628.74,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.816,628.955,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.565,629.389,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.357,629.822,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.126,630.447,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[257.988,631.01,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[257.934,631.586,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.176,631.632,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.354,631.053,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.421,630.764,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.491,630.42,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.565,630.018,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.642,629.555,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.724,629.051,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.81,628.521,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[258.897,627.967,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1677\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":58,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[420.402,620.794,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[420.018,621.345,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[419.636,621.903,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[419.257,622.466,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[418.884,623.033,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[418.516,623.601,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[418.156,624.169,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[417.806,624.734,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[417.466,625.294,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[417.138,625.848,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[416.823,626.392,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[416.524,626.925,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[416.24,627.445,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[415.975,627.948,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[415.728,628.433,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[415.502,628.897,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[415.298,629.338,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[415.116,629.753,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[414.959,630.14,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[414.828,630.497,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[414.646,631.111,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[414.578,631.576,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[414.79,632.168,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[415.194,632.286,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[415.483,632.244,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[415.831,632.139,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[416.239,631.971,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[416.464,631.863,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[416.703,631.739,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[416.954,631.598,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[417.218,631.441,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[417.493,631.265,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[417.779,631.071,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[418.074,630.857,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[418.378,630.622,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[418.688,630.365,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[419.005,630.085,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[419.326,629.779,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[419.65,629.446,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[419.976,629.085,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[420.301,628.692,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[420.625,628.265,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[420.944,627.803,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[421.26,627.312,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[421.57,626.81,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[421.873,626.298,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[422.169,625.78,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[422.454,625.259,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[422.728,624.738,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[422.989,624.22,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[423.235,623.707,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[423.465,623.204,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[423.678,622.711,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[423.871,622.234,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[424.044,621.773,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[424.196,621.332,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[424.324,620.913,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[424.429,620.519,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[424.509,620.152,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[424.562,619.815,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[424.589,619.236,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[424.417,618.636,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[424.157,618.43,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[423.781,618.39,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[423.55,618.433,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[423.291,618.518,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[423.005,618.646,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[422.693,618.815,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[422.356,619.025,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[421.996,619.273,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[421.613,619.561,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[421.21,619.887,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[420.787,620.252,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1673\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":59,\"ty\":4,\"nm\":\".dot2\",\"cl\":\"dot2\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[465.696,656.832,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[465.843,656.419,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.046,655.905,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.311,655.299,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.467,654.964,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.638,654.609,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.826,654.235,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.029,653.841,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.248,653.429,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.481,652.999,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.727,652.55,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.987,652.084,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.258,651.599,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.54,651.097,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.832,650.575,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.131,650.036,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.435,649.485,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.743,648.926,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.054,648.359,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.366,647.79,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.678,647.218,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.988,646.648,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[471.295,646.082,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[471.597,645.523,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[471.894,644.973,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.182,644.435,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.462,643.913,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.732,643.408,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.99,642.924,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[473.235,642.463,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[473.465,642.028,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[473.68,641.622,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[473.878,641.247,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[474.057,640.905,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[474.357,640.331,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[474.646,639.776,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[474.34,640.338,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[474.023,640.852,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[473.624,641.473,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[473.396,641.826,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[473.15,642.208,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.888,642.62,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.61,643.061,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.319,643.532,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.015,644.03,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[471.7,644.556,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[471.376,645.108,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[471.044,645.685,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.707,646.285,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.365,646.905,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.022,647.543,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.679,648.195,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.339,648.86,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.004,649.532,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.677,650.208,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.359,650.882,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.055,651.551,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.768,652.208,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.499,652.848,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.253,653.464,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.033,654.05,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.842,654.597,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.685,655.099,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.551,655.551,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.392,655.97,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.205,656.356,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[465.992,656.708,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[465.753,657.026,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.639215707779,0.839215695858,0.615686297417,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1671\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":60,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[640.676,188.45,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.691,188.261,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.725,188.063,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.777,187.858,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.847,187.646,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.933,187.431,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.036,187.212,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.154,186.994,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.288,186.777,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.436,186.563,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.598,186.355,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.773,186.156,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.961,185.966,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.161,185.79,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.373,185.628,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.595,185.484,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.828,185.36,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.323,185.179,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.854,185.105,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.416,185.152,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.988,185.264,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.512,185.249,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.988,185.136,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.425,184.963,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.63,184.866,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.828,184.767,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.019,184.669,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.386,184.492,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.911,184.309,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.434,184.269,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.99,184.385,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.399,184.542,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.62,184.638,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.854,184.744,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[650.103,184.855,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[650.37,184.967,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[650.655,185.077,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[650.963,185.179,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[651.593,185.36,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[651.862,185.453,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.095,185.55,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.291,185.651,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.451,185.755,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.573,185.862,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.658,185.973,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.704,186.087,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.713,186.203,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.683,186.323,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.615,186.444,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.508,186.568,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.364,186.694,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[652.183,186.82,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[651.964,186.948,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[651.708,187.075,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[651.417,187.203,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[651.09,187.329,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[650.729,187.454,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[650.335,187.577,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.907,187.698,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[649.449,187.814,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.961,187.927,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[648.444,188.034,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.9,188.136,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[647.331,188.231,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.738,188.319,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[646.123,188.399,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[645.488,188.469,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.835,188.529,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.169,188.575,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.49,188.607,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.8,188.626,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.101,188.635,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.393,188.635,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.679,188.629,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"329\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":61,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[613.736,119.525,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.988,119.068,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.227,118.563,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.456,118.019,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.679,117.441,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.896,116.835,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.111,116.208,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.325,115.565,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.541,114.911,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.761,114.251,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.987,113.59,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.221,112.93,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.463,112.276,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.717,111.632,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.982,110.999,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.262,110.38,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.557,109.778,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.869,109.192,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.198,108.625,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.546,108.077,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.914,107.547,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.303,107.035,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.713,106.54,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.146,106.059,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.589,105.6,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.034,105.166,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.477,104.761,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.917,104.384,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.352,104.037,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.781,103.72,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.201,103.434,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.611,103.181,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.008,102.96,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.392,102.772,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.76,102.617,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.111,102.497,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.442,102.41,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.041,102.338,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.545,102.401,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.758,102.484,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.943,102.599,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.099,102.747,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.224,102.928,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.319,103.14,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.382,103.383,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.412,103.657,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.409,103.959,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.372,104.291,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.301,104.65,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.195,105.035,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.055,105.446,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.881,105.881,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.682,106.313,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.464,106.728,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.226,107.126,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.968,107.508,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.688,107.875,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.387,108.228,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.065,108.569,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.723,108.9,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.361,109.22,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.979,109.534,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.58,109.84,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.164,110.143,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.732,110.443,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.288,110.742,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.833,111.042,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.369,111.345,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.899,111.653,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.425,111.969,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.951,112.294,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.479,112.63,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.013,112.98,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.557,113.347,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[618.114,113.732,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.688,114.138,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[617.284,114.566,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.905,115.021,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.557,115.504,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[616.244,116.017,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.971,116.564,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.743,117.145,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.517,117.697,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[615.267,118.176,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.995,118.585,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.704,118.923,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.395,119.192,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[614.072,119.392,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[613.736,119.525,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"411\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":62,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[622.845,154.534,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.152,153.788,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.483,153.094,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.834,152.451,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.205,151.862,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.593,151.329,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.996,150.853,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.413,150.434,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.841,150.075,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.278,149.776,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.721,149.539,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.17,149.363,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.621,149.25,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.073,149.201,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.523,149.215,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.969,149.292,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.408,149.434,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.84,149.64,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.261,149.911,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.67,150.245,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.065,150.642,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.443,151.103,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.803,151.627,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.148,152.192,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.485,152.765,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.81,153.344,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.122,153.926,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.417,154.512,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.695,155.1,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.953,155.689,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.19,156.278,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.405,156.867,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.597,157.455,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.766,158.042,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.91,158.626,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.03,159.207,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.126,159.785,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.199,160.36,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.249,160.932,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.277,161.5,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.284,162.064,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.271,162.624,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.241,163.181,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.194,163.733,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.134,164.283,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.062,164.829,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.981,165.372,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.894,165.913,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.803,166.451,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.712,166.988,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.624,167.523,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.544,168.058,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.474,168.593,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.398,169.109,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.287,169.573,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.139,169.984,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.958,170.342,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.746,170.645,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.503,170.894,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.232,171.088,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.936,171.227,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.617,171.312,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.276,171.342,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.916,171.318,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.539,171.24,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.148,171.109,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.746,170.926,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.334,170.691,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.915,170.406,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.491,170.071,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.064,169.687,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.638,169.257,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.214,168.78,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.793,168.259,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.38,167.696,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.974,167.091,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.579,166.447,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.195,165.766,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.825,165.05,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.471,164.3,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.133,163.519,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.813,162.709,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.511,161.872,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.227,161.013,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.96,160.135,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.711,159.239,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.484,158.325,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.282,157.396,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.106,156.453,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.96,155.498,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.845,154.534,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"328\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":63,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[548.335,42.454,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.471,42.827,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.591,43.213,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.696,43.609,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.786,44.015,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.86,44.429,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.92,44.849,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.965,45.275,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.996,45.706,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[549.013,46.14,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[549.018,46.576,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[549.01,47.014,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.991,47.453,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.962,47.892,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.922,48.329,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.874,48.765,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.819,49.198,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.756,49.629,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.689,50.055,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.617,50.478,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.542,50.896,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.466,51.309,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.388,51.715,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.302,52.106,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.208,52.481,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[548.105,52.838,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.995,53.176,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.877,53.493,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.753,53.789,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.623,54.062,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.488,54.312,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.348,54.538,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.205,54.739,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.058,54.914,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[546.91,55.063,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[546.76,55.186,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[546.609,55.281,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[546.459,55.349,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[546.019,55.386,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.879,55.342,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.744,55.271,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.615,55.172,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.492,55.045,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.376,54.891,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.268,54.71,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.169,54.504,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.08,54.271,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.001,54.014,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.933,53.733,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.876,53.428,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.825,53.105,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.756,52.776,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.667,52.442,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.562,52.102,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.445,51.757,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.319,51.404,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.187,51.045,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.051,50.679,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.915,50.306,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.781,49.927,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.651,49.542,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.529,49.152,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.416,48.757,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.316,48.359,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.229,47.959,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.159,47.559,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.107,47.159,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.075,46.762,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.065,46.369,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.078,45.983,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.117,45.605,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.182,45.238,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.275,44.884,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.397,44.547,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.549,44.228,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.732,43.931,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.948,43.658,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.197,43.413,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.479,43.2,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[544.796,43.021,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.144,42.877,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.507,42.753,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[545.882,42.649,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[546.269,42.566,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[546.667,42.502,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.073,42.459,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.488,42.436,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[547.909,42.434,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"330\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":64,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[497.037,46.056,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.981,45.887,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.951,45.727,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.947,45.575,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.967,45.433,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.011,45.3,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.079,45.177,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.17,45.065,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.282,44.963,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.416,44.872,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.569,44.793,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.741,44.725,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.931,44.668,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.137,44.624,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.359,44.591,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.845,44.563,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.106,44.567,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.378,44.584,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.659,44.613,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.949,44.654,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.242,44.708,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.497,44.781,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.704,44.874,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.867,44.983,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.987,45.108,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.068,45.245,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.113,45.393,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.123,45.551,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.102,45.716,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[501.053,45.887,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.978,46.062,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.88,46.24,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.761,46.42,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.625,46.601,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.472,46.78,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.307,46.958,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[500.132,47.134,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.949,47.306,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.761,47.473,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.571,47.636,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.38,47.793,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.193,47.943,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[499.011,48.087,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.837,48.224,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.674,48.353,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.524,48.475,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.391,48.589,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.277,48.695,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.186,48.792,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.119,48.882,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.079,48.964,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[498.04,49.035,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.996,49.095,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.949,49.145,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.897,49.184,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.786,49.229,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.549,49.196,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.49,49.163,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.433,49.121,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.378,49.069,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.325,49.01,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.274,48.942,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.227,48.866,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.183,48.783,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.143,48.693,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.106,48.596,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.074,48.494,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.046,48.386,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.022,48.272,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.002,48.155,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.987,48.033,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.976,47.908,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.968,47.78,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.965,47.649,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.965,47.516,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.968,47.382,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.974,47.246,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.981,47.107,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.988,46.966,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[496.995,46.823,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.002,46.683,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.009,46.546,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.016,46.413,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.023,46.286,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.03,46.167,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[497.037,46.056,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"29\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":65,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[629.73,209.675,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.763,210.028,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.767,210.404,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.744,210.803,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.694,211.222,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.622,211.66,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.528,212.115,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.415,212.586,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.285,213.072,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.139,213.57,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.98,214.079,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.811,214.598,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.633,215.124,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.449,215.656,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.26,216.191,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.07,216.729,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.881,217.265,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.695,217.799,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.516,218.328,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.344,218.85,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.184,219.362,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.028,219.862,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.87,220.349,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.71,220.821,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.548,221.277,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.385,221.714,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.222,222.131,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.059,222.527,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.897,222.9,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.738,223.248,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.58,223.571,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.426,223.866,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.276,224.133,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.131,224.371,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.992,224.577,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.859,224.753,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.733,224.896,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.508,225.082,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.13,224.938,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.092,224.803,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.069,224.632,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.06,224.426,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.067,224.185,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.089,223.91,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.128,223.6,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.183,223.258,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.243,222.887,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.301,222.491,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.357,222.07,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.412,221.627,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.468,221.163,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.526,220.679,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.587,220.178,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.651,219.661,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.721,219.13,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.796,218.587,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.878,218.034,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.969,217.473,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.067,216.907,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.176,216.338,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.295,215.768,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.425,215.202,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.566,214.64,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.72,214.087,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.887,213.545,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.067,213.018,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.261,212.51,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.469,212.024,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.692,211.564,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.928,211.135,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.18,210.74,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.445,210.384,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.726,210.073,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.02,209.811,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.328,209.603,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.649,209.455,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.211,209.278,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.817,209.269,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.86,209.424,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.816,209.538,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"532\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":66,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[621.553,542.61,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.405,543.046,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.266,543.47,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.138,543.88,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.02,544.274,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.915,544.649,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.823,545.004,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.744,545.336,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.68,545.643,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.6,546.175,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.59,546.585,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.185,546.9,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.276,546.404,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.287,545.955,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.351,545.591,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.468,545.199,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.641,544.808,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.011,544.303,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.481,543.963,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.074,543.466,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.466,543.056,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.796,542.635,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.116,542.096,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.27,541.633,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.651,541.163,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.198,541.321,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.626,541.613,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.186,541.869,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1440\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":67,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[658.553,541.954,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.241,542.767,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[657.927,543.583,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[657.613,544.397,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[657.302,545.203,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[656.998,545.996,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[656.701,546.774,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[656.415,547.531,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[656.142,548.265,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[655.884,548.973,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[655.641,549.653,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[655.415,550.303,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[655.208,550.922,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[655.021,551.51,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.853,552.065,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.706,552.589,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.58,553.082,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.475,553.546,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.39,553.983,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.326,554.393,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.282,554.764,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.258,555.094,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.27,555.631,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[654.428,556.12,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[655.022,556.11,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[655.538,555.637,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[655.931,555.145,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[656.139,554.851,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[656.354,554.525,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[656.573,554.17,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[656.797,553.788,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[657.023,553.381,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[657.25,552.949,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[657.479,552.496,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[657.706,552.022,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[657.932,551.53,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.156,551.021,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.376,550.498,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.591,549.963,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.797,549.419,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.973,548.873,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.118,548.33,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.233,547.792,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.32,547.263,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.38,546.744,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.414,546.237,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.425,545.744,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.415,545.267,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.384,544.806,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.336,544.363,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.273,543.938,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.196,543.532,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.108,543.145,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[659.012,542.777,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.91,542.427,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.805,542.096,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.699,541.782,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.595,541.485,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.497,541.205,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.326,540.686,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.212,540.216,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.18,539.78,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.256,539.364,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.469,538.952,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.946,539.382,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.907,539.765,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.847,540.217,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.767,540.736,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.668,541.316,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[658.553,541.954,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1436\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":68,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[636.494,440.925,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.753,441.191,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.335,441.513,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.656,441.569,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.994,441.556,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.347,441.474,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.716,441.324,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.096,441.107,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.488,440.824,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.89,440.477,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.299,440.067,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.714,439.595,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.134,439.064,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.556,438.476,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.98,437.832,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.404,437.134,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.825,436.386,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.21,435.604,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.552,434.796,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.849,433.968,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.105,433.125,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.32,432.271,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.495,431.411,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.633,430.548,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.735,429.686,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.802,428.83,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.838,427.981,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.843,427.143,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.82,426.318,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.771,425.51,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.698,424.72,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.605,423.951,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.492,423.204,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.364,422.481,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.222,421.784,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[644.069,421.114,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.909,420.472,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.744,419.859,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.577,419.277,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.411,418.725,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.251,418.204,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[643.098,417.715,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.956,417.258,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.829,416.834,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.721,416.442,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.635,416.083,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.572,415.758,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.501,415.497,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[642.019,415.193,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.68,415.461,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.491,415.694,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.292,415.989,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[641.083,416.343,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.866,416.755,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.642,417.221,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.414,417.738,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[640.182,418.304,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.948,418.914,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.713,419.566,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.479,420.257,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.248,420.982,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[639.019,421.739,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.796,422.524,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.578,423.333,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.366,424.164,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[638.163,425.012,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.968,425.875,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.783,426.749,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.608,427.631,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.444,428.517,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.291,429.405,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.15,430.291,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[637.02,431.173,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.892,432.05,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.765,432.919,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.644,433.778,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.53,434.625,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.428,435.456,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.34,436.269,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.269,437.06,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.217,437.827,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.188,438.566,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.184,439.275,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.207,439.95,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[636.259,440.588,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1438\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":69,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[624.88,481.986,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.327,481.422,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.755,480.874,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.164,480.343,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.555,479.833,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.926,479.343,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.278,478.878,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.611,478.438,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.925,478.025,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.22,477.64,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.497,477.285,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.757,476.961,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629,476.668,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.227,476.408,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.639,475.989,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.16,475.624,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.605,475.986,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.56,476.419,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.504,476.688,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.425,476.99,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.325,477.322,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.204,477.683,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.064,478.071,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.905,478.482,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.729,478.914,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.536,479.365,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.328,479.832,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.107,480.312,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.873,480.802,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.628,481.299,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.373,481.801,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.11,482.304,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.84,482.805,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.565,483.302,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.287,483.791,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.006,484.27,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.724,484.736,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.443,485.186,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.164,485.617,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.877,486.03,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.571,486.429,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.251,486.812,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.919,487.179,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.578,487.53,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.232,487.865,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.883,488.182,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.533,488.481,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.186,488.761,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.845,489.021,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.51,489.261,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.186,489.479,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.577,489.847,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.033,490.117,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.572,490.28,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.064,490.305,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.794,489.883,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.855,489.458,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[619.937,489.189,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.056,488.881,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.21,488.534,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.4,488.15,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.624,487.735,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.88,487.293,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.168,486.827,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.485,486.338,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.829,485.831,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.2,485.307,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.596,484.77,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.015,484.223,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.454,483.668,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.913,483.109,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.389,482.547,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.88,481.986,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1312\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":70,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[580.44,619.428,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[580.227,619.861,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[580,620.306,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[579.76,620.759,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[579.506,621.219,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[579.242,621.682,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[578.968,622.145,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[578.686,622.605,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[578.397,623.059,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[578.103,623.503,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[577.805,623.935,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[577.505,624.351,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[577.205,624.748,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[576.906,625.122,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[576.609,625.47,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[576.317,625.789,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[576.028,626.076,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[575.735,626.333,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[575.44,626.56,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[575.144,626.76,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.847,626.932,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.55,627.08,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.256,627.202,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.963,627.302,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.39,627.433,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.836,627.481,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.309,627.45,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[571.813,627.345,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[571.353,627.17,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.935,626.929,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.392,626.449,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.959,625.836,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.735,625.358,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.564,624.828,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.472,624.243,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.47,623.929,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.496,623.603,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.549,623.267,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.629,622.923,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.736,622.572,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[569.868,622.217,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.024,621.86,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.203,621.502,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.404,621.146,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.627,620.793,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[570.869,620.445,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[571.129,620.105,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[571.407,619.773,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[571.7,619.453,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.008,619.144,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.329,618.85,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[572.661,618.572,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.003,618.311,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.354,618.069,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[573.713,617.847,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.076,617.646,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.444,617.467,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[574.815,617.312,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[575.187,617.181,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[575.558,617.075,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[575.928,616.994,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[576.295,616.94,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[576.657,616.912,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[577.015,616.91,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[577.37,616.932,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[577.718,616.98,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[578.058,617.051,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[578.387,617.147,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[578.705,617.266,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[579.008,617.408,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[579.567,617.76,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[580.054,618.199,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[580.463,618.72,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1637\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":71,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[141.769,643.428,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[141.865,643.349,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[141.979,643.256,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.109,643.149,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.255,643.028,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.413,642.895,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.584,642.751,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.765,642.597,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.955,642.434,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[143.152,642.262,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[143.355,642.083,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[143.562,641.897,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[143.772,641.706,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[143.982,641.509,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[144.192,641.309,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[144.405,641.101,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[144.622,640.886,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[144.841,640.666,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[145.061,640.44,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[145.282,640.209,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[145.503,639.975,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[145.723,639.739,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[145.94,639.501,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.154,639.264,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.364,639.027,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.568,638.792,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.766,638.561,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.956,638.335,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.138,638.115,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.31,637.902,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.471,637.698,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.62,637.504,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.757,637.321,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.881,637.151,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.989,636.995,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.083,636.854,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.22,636.622,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[148.113,636.445,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.981,636.538,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.856,636.649,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.72,636.784,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.642,636.863,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.556,636.95,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.46,637.047,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.354,637.153,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.235,637.27,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[147.104,637.398,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.96,637.537,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.803,637.689,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.632,637.852,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.449,638.027,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.254,638.214,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[146.046,638.414,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[145.829,638.625,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[145.601,638.849,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[145.366,639.083,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[145.124,639.328,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[144.878,639.583,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[144.629,639.847,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[144.38,640.118,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[144.134,640.397,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[143.894,640.681,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[143.662,640.969,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[143.442,641.258,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[143.236,641.547,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[143.04,641.818,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.854,642.072,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.677,642.306,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.512,642.52,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.359,642.715,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.218,642.89,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[142.091,643.044,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[141.977,643.178,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[141.879,643.29,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[141.796,643.382,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[141.68,643.501,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1682\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":72,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[93.816,623.805,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.16,623.677,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.475,623.548,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.757,623.419,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.007,623.29,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.222,623.164,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.401,623.041,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.543,622.923,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.647,622.811,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.658,622.452,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.555,622.392,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.408,622.347,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.218,622.318,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[95.013,622.26,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.804,622.161,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.59,622.025,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.369,621.857,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[94.141,621.661,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.904,621.441,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.658,621.203,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.402,620.949,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.136,620.684,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.859,620.412,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.571,620.135,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.272,619.858,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.962,619.583,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.64,619.314,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.307,619.052,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[90.965,618.802,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[90.612,618.566,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[90.25,618.345,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[89.879,618.143,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[89.501,617.962,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[89.117,617.802,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[88.727,617.668,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[88.333,617.559,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[87.938,617.477,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[87.541,617.425,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[87.146,617.404,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[86.754,617.414,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[86.367,617.456,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[85.988,617.533,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[85.619,617.643,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[85.264,617.779,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[84.928,617.934,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[84.61,618.108,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[84.312,618.3,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[84.035,618.509,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[83.781,618.733,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[83.549,618.97,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[83.342,619.22,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[83.159,619.48,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[83.003,619.75,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[82.873,620.027,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[82.77,620.31,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[82.695,620.597,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[82.632,621.177,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[82.686,621.754,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[82.758,622.037,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[82.86,622.313,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[82.992,622.583,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[83.154,622.843,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[83.346,623.092,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[83.568,623.329,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[83.819,623.553,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[84.099,623.762,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[84.406,623.955,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[84.741,624.13,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[85.103,624.288,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[85.49,624.426,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[85.902,624.544,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[86.337,624.652,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[86.795,624.749,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[87.272,624.832,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[87.768,624.897,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[88.28,624.941,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[88.807,624.961,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[89.345,624.954,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[89.894,624.919,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[90.45,624.854,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.012,624.758,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[91.577,624.63,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.142,624.471,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[92.705,624.279,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.264,624.057,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[93.816,623.805,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1684\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":73,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[206.153,627.891,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[205.689,627.417,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[205.221,626.951,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[204.75,626.495,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[204.275,626.053,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.798,625.628,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.319,625.221,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.839,624.835,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.357,624.474,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[201.876,624.138,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[201.396,623.83,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[200.916,623.552,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[200.438,623.305,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.962,623.093,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.489,622.915,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.022,622.772,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[198.567,622.663,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[198.125,622.588,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[197.699,622.546,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[197.288,622.538,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[196.896,622.563,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[196.523,622.62,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[196.171,622.709,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.84,622.828,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.533,622.976,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.25,623.152,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.992,623.356,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.76,623.584,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.555,623.836,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.379,624.11,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.23,624.405,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.111,624.717,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.022,625.046,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.962,625.39,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.933,625.747,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.935,626.114,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[193.967,626.489,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.03,626.871,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.124,627.258,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.248,627.646,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.403,628.035,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.588,628.422,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[194.802,628.805,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.045,629.183,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.317,629.552,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.613,629.931,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[195.93,630.333,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[196.267,630.753,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[196.621,631.184,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[196.993,631.621,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[197.381,632.058,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[197.783,632.49,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[198.198,632.912,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[198.624,633.321,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.059,633.714,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.502,634.086,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[199.951,634.435,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[200.405,634.759,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[200.86,635.056,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[201.316,635.324,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[201.769,635.561,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.219,635.767,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[202.662,635.942,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.096,636.084,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.519,636.194,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[203.928,636.273,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[204.32,636.322,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[204.693,636.34,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[205.044,636.331,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[205.369,636.294,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[205.666,636.233,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[205.932,636.15,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.163,636.046,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.356,635.925,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.507,635.79,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.626,635.619,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.806,635.101,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.867,634.761,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.909,634.37,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.932,633.934,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.936,633.455,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.921,632.938,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.887,632.387,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.835,631.804,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.765,631.196,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.676,630.564,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.571,629.914,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.448,629.249,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.309,628.574,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[206.153,627.891,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1676\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":74,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[538.451,674.289,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[538.018,674.885,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[538.189,675.491,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[538.708,676.103,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[539.058,676.478,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[539.455,676.865,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[539.888,677.243,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[540.345,677.593,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[540.815,677.897,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[541.285,678.143,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[541.74,678.318,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[542.165,678.413,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[542.544,678.421,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[542.987,678.263,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.269,677.737,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.331,677.169,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.293,676.498,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.215,676.012,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[543.099,675.508,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[542.948,674.999,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[542.764,674.497,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[542.552,674.014,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[542.189,673.353,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[541.786,672.798,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[541.358,672.383,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[540.921,672.132,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[540.395,672.064,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[539.943,672.303,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[539.499,672.847,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[539.147,673.436,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1638\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":75,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[472.549,630.853,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.682,630.347,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.748,629.891,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.703,629.276,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.464,628.708,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.024,628.184,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[471.635,627.899,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[471.179,627.671,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.929,627.581,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.667,627.506,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.392,627.447,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.108,627.403,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.816,627.376,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.516,627.365,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.212,627.369,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.905,627.389,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.596,627.425,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.288,627.475,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.981,627.54,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.679,627.618,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.382,627.711,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.092,627.816,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.812,627.933,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.543,628.062,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.287,628.202,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.045,628.352,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[465.61,628.677,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[465.25,629.032,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[464.88,629.596,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[464.766,630.172,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[464.892,630.767,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[465.23,631.381,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[465.556,631.793,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[465.949,632.2,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.394,632.59,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.632,632.775,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[466.877,632.952,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.127,633.117,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.38,633.269,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.633,633.405,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[467.884,633.524,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.13,633.622,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[468.596,633.746,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.007,633.754,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.467,633.495,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[469.911,633.093,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.251,632.822,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.613,632.556,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[470.984,632.297,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[471.355,632.051,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[471.713,631.821,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[472.05,631.609,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1425\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":76,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[630.572,266.837,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.953,266.85,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.331,266.869,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.709,266.894,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.093,266.924,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.485,266.959,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.891,266.997,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.314,267.037,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.758,267.078,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.227,267.119,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.725,267.159,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.256,267.197,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.809,267.229,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.381,267.257,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.974,267.282,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.593,267.306,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.239,267.331,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.624,267.383,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.145,267.448,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.604,267.677,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[620.857,267.918,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.407,268.096,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[621.916,268.194,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.51,268.259,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[622.83,268.273,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.161,268.272,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.497,268.253,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[623.834,268.215,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.168,268.154,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.52,268.083,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[624.901,268.009,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.31,267.933,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[625.743,267.856,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.197,267.777,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[626.67,267.697,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.158,267.616,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[627.659,267.534,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.17,267.453,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[628.687,267.372,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.206,267.291,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[629.726,267.212,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.243,267.133,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[630.753,267.057,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.254,266.982,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.742,266.909,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.215,266.839,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.67,266.772,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.103,266.707,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.513,266.646,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.896,266.589,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.251,266.535,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.865,266.441,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[635.34,266.364,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.892,266.403,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.325,266.49,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[634.002,266.537,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.654,266.585,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[633.284,266.634,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.894,266.681,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.487,266.725,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[632.065,266.767,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[631.629,266.803,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"654\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":77,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[-2.204,519.466,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-2.565,519.345,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-2.879,519.241,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.147,519.156,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.368,519.088,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.542,519.037,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.671,519.002,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.755,518.982,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.795,518.977,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.794,518.986,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.754,519.007,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.677,519.038,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.561,519.081,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.401,519.138,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-3.197,519.209,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-2.951,519.293,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-2.663,519.391,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-2.335,519.5,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-1.97,519.622,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-1.568,519.754,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-1.133,519.897,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-0.668,520.05,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-0.174,520.212,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[0.346,520.382,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[0.887,520.559,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[1.447,520.743,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[2.023,520.931,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[2.61,521.124,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[3.206,521.32,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[3.807,521.518,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.409,521.717,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[5.008,521.916,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[5.601,522.114,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[6.184,522.309,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[6.753,522.501,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[7.304,522.689,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[7.834,522.871,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[8.339,523.047,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[8.816,523.215,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[9.261,523.374,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[9.671,523.524,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.043,523.663,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.37,523.798,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.645,523.935,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.871,524.073,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.049,524.212,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.181,524.349,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.268,524.483,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.313,524.614,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.316,524.74,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.281,524.86,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.207,524.973,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[11.097,525.079,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.953,525.175,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.776,525.263,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.568,525.34,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.332,525.407,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[10.068,525.463,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[9.779,525.507,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[9.468,525.538,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[9.135,525.557,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[8.784,525.563,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[8.417,525.556,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[8.036,525.535,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[7.644,525.5,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[7.244,525.452,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[6.838,525.389,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[6.43,525.312,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[6.023,525.221,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[5.62,525.116,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[5.224,524.996,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.84,524.863,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.457,524.711,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[4.059,524.536,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[3.649,524.338,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[3.23,524.119,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[2.805,523.881,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[2.377,523.626,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[1.95,523.355,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[1.526,523.069,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[1.108,522.771,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[0.699,522.462,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[0.301,522.143,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-0.083,521.817,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-0.451,521.486,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-0.801,521.15,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-1.13,520.811,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-1.436,520.472,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-1.719,520.134,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-1.975,519.798,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[-2.204,519.466,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1688\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":78,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[29.955,468.393,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.479,468.815,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.024,469.221,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.585,469.609,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.16,469.975,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.743,470.318,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.333,470.634,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.923,470.922,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.512,471.178,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.095,471.399,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.667,471.584,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.218,471.74,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.721,471.894,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.176,472.046,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.586,472.194,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.952,472.334,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.277,472.464,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.561,472.583,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.806,472.687,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.015,472.776,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.189,472.847,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.329,472.9,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.438,472.933,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.516,472.944,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.566,472.934,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.562,472.768,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.514,472.666,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.446,472.541,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.359,472.394,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.255,472.225,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.136,472.035,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.003,471.824,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.859,471.594,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.706,471.346,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.544,471.081,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.377,470.802,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.207,470.51,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.035,470.207,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.863,469.896,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.688,469.575,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.49,469.241,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.267,468.894,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.022,468.536,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.756,468.168,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.472,467.794,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.17,467.415,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.854,467.033,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.524,466.651,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.184,466.27,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.834,465.893,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.477,465.522,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.116,465.16,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.751,464.807,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.386,464.468,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.022,464.142,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.661,463.834,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.305,463.544,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.956,463.274,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.616,463.027,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.286,462.804,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.969,462.606,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.666,462.436,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.379,462.293,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[30.108,462.181,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.856,462.098,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.624,462.048,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.412,462.03,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.221,462.044,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.054,462.092,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.907,462.17,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.775,462.272,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.659,462.399,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.558,462.552,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.476,462.732,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.411,462.94,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.366,463.175,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.341,463.439,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.337,463.731,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.356,464.049,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.398,464.394,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.464,464.765,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.556,465.158,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.674,465.574,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.818,466.01,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[28.99,466.463,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.189,466.931,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.416,467.41,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.671,467.899,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[29.955,468.393,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1689\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":79,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[43.951,532.325,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.74,532.088,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.511,531.823,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.267,531.53,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.011,531.211,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.745,530.866,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.473,530.497,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.196,530.105,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.918,529.693,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.64,529.261,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.366,528.813,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.089,528.349,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.81,527.87,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.529,527.379,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.246,526.877,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.962,526.366,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.678,525.849,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.396,525.328,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.117,524.804,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.841,524.282,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.57,523.762,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.305,523.247,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.048,522.74,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.801,522.242,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.564,521.758,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.339,521.288,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.129,520.835,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.933,520.402,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.755,519.991,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.595,519.604,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.455,519.244,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.337,518.912,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.242,518.611,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.171,518.342,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.126,518.107,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.12,517.748,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.161,517.627,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.233,517.546,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.336,517.508,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.472,517.509,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.632,517.531,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.811,517.569,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.006,517.628,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.214,517.708,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.433,517.812,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.662,517.941,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.897,518.097,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.139,518.279,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.385,518.49,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.634,518.728,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.887,518.994,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.142,519.287,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.398,519.607,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.657,519.952,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.917,520.32,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.179,520.711,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.443,521.123,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.709,521.552,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.977,521.996,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.248,522.453,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.522,522.919,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.799,523.39,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.079,523.865,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.363,524.337,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.651,524.803,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.942,525.26,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.237,525.701,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.535,526.122,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.836,526.518,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.136,526.886,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.404,527.257,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.632,527.635,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.824,528.017,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.979,528.399,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.1,528.78,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.188,529.155,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.245,529.522,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.273,529.878,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.273,530.22,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.248,530.545,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.198,530.85,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.126,531.133,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[45.033,531.391,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.922,531.622,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.794,531.824,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.65,531.994,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.492,532.131,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.322,532.232,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[44.142,532.298,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[43.951,532.325,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1357\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":80,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[297.778,57.224,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[297.782,56.922,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[297.827,56.629,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[297.917,56.348,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[298.051,56.079,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[298.233,55.823,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[298.462,55.583,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[298.74,55.36,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[299.07,55.153,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[299.451,54.966,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[299.878,54.776,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[300.34,54.572,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[300.832,54.359,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[301.346,54.137,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[301.878,53.91,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[302.421,53.681,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[302.972,53.45,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[303.526,53.222,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[304.079,52.997,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[304.628,52.776,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[305.17,52.563,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[305.701,52.358,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[306.22,52.163,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[306.724,51.979,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[307.212,51.806,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[307.681,51.647,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[308.131,51.501,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[308.561,51.369,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[308.968,51.253,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[309.354,51.153,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[309.717,51.069,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[310.057,51.001,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[310.373,50.951,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[310.665,50.917,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[310.934,50.901,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[311.178,50.902,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[311.398,50.92,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[311.594,50.956,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[311.765,51.009,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[311.912,51.079,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[312.021,51.169,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[312.085,51.282,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[312.106,51.416,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[312.083,51.57,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[312.019,51.743,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[311.915,51.934,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[311.773,52.141,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[311.593,52.363,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[311.378,52.599,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[311.131,52.847,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[310.852,53.105,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[310.545,53.373,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[310.211,53.647,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[309.853,53.927,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[309.473,54.211,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[309.074,54.497,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[308.658,54.784,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[308.228,55.07,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[307.786,55.353,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[307.334,55.632,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[306.875,55.904,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[306.413,56.17,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[305.948,56.426,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[305.483,56.672,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[305.021,56.907,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[304.563,57.128,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[304.113,57.335,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[303.671,57.527,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[303.239,57.703,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[302.82,57.862,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[302.412,58.002,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[302.011,58.123,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[301.62,58.225,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[301.24,58.309,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[300.87,58.374,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[300.513,58.422,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[300.17,58.451,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[299.841,58.462,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[299.528,58.456,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[299.234,58.433,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[298.959,58.393,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[298.706,58.337,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[298.476,58.266,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[298.272,58.18,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[298.096,58.079,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[297.95,57.965,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[297.838,57.838,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[297.762,57.7,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[297.724,57.55,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[297.728,57.391,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[297.778,57.224,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"22\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":81,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[40.669,194.084,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.908,193.737,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.128,193.387,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.329,193.038,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.509,192.69,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.669,192.347,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.807,192.011,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.922,191.685,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.013,191.372,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.08,191.073,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.122,190.787,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.139,190.514,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.131,190.256,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.098,190.014,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[42.041,189.788,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.96,189.578,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.855,189.386,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.727,189.211,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.577,189.055,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.406,188.916,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.215,188.797,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[41.004,188.696,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.776,188.613,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.531,188.55,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.27,188.505,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.996,188.48,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.71,188.472,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.413,188.483,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.106,188.512,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.793,188.559,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.473,188.624,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.15,188.705,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.825,188.803,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.499,188.917,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.175,189.046,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.854,189.191,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.538,189.35,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.229,189.523,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.929,189.709,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.639,189.894,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.358,190.058,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.086,190.205,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.822,190.337,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.566,190.455,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.317,190.564,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.075,190.663,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.841,190.756,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.613,190.843,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.394,190.927,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.182,191.009,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.978,191.09,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.783,191.172,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.597,191.254,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.421,191.339,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.257,191.426,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.104,191.517,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.964,191.612,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.839,191.712,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.729,191.817,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.635,191.927,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.56,192.043,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.504,192.165,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.47,192.293,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.46,192.427,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.474,192.568,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.515,192.714,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.584,192.868,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.685,193.027,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.819,193.194,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[31.988,193.361,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.191,193.522,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.427,193.674,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.693,193.818,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[32.989,193.952,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.313,194.075,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[33.662,194.188,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.035,194.289,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.43,194.377,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[34.844,194.451,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.277,194.512,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[35.726,194.558,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.188,194.589,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[36.663,194.603,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.149,194.601,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[37.642,194.582,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.142,194.545,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[38.646,194.49,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.153,194.417,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[39.66,194.325,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.166,194.214,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[40.669,194.084,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"370\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":82,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[26.292,212.525,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.371,212.44,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.422,212.366,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.441,212.251,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.408,212.21,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.345,212.18,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.252,212.161,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.137,212.143,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.028,212.088,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.924,211.996,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.823,211.874,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.724,211.724,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.624,211.55,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.521,211.357,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.414,211.148,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.3,210.925,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.179,210.693,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.05,210.454,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.911,210.211,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.761,209.967,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.6,209.722,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.427,209.481,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.242,209.245,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.043,209.015,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.831,208.793,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.606,208.581,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.367,208.381,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.115,208.192,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.849,208.018,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.57,207.858,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.277,207.714,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.972,207.586,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.655,207.475,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.325,207.381,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.984,207.306,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.631,207.25,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.268,207.213,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.898,207.195,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.528,207.195,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.162,207.213,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.802,207.249,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.449,207.303,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.105,207.372,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.771,207.458,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.449,207.559,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.14,207.674,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.847,207.803,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.571,207.944,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.313,208.097,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.074,208.26,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.857,208.433,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.662,208.614,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.491,208.803,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.346,208.998,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.226,209.197,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.133,209.401,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.069,209.607,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.034,209.815,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.028,210.023,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.053,210.23,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.109,210.436,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.196,210.638,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.314,210.836,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.464,211.029,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.646,211.216,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[15.859,211.396,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.102,211.568,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.374,211.734,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.666,211.9,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[16.978,212.065,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.31,212.226,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[17.661,212.38,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.033,212.527,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.423,212.662,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[18.832,212.786,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.258,212.895,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[19.702,212.989,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.161,213.067,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[20.635,213.127,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.122,213.169,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[21.621,213.193,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.129,213.196,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[22.645,213.18,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.167,213.145,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[23.692,213.089,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.219,213.014,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[24.745,212.919,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.268,212.806,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[25.784,212.674,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[26.292,212.525,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"457\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":83,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[188.787,38.105,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[188.359,38.084,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.908,38.069,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.434,38.061,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.938,38.059,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.42,38.062,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.881,38.069,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.322,38.081,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.746,38.098,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.156,38.12,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.554,38.147,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.943,38.178,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.326,38.214,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.704,38.255,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.082,38.299,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.46,38.348,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.841,38.4,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.23,38.456,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.627,38.514,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.035,38.576,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.458,38.64,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[176.898,38.706,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[176.356,38.773,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[175.837,38.842,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[175.342,38.912,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[174.874,38.982,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[174.435,39.052,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[174.027,39.121,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[173.652,39.19,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[173.314,39.258,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[173.012,39.324,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.75,39.388,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.529,39.45,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.351,39.509,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.217,39.564,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.128,39.617,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.086,39.665,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.09,39.711,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.119,39.788,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.17,39.899,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.244,40.041,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.343,40.211,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.467,40.403,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.617,40.615,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.794,40.842,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[172.999,41.083,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[173.23,41.332,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[173.487,41.588,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[173.771,41.848,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[174.08,42.108,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[174.413,42.367,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[174.769,42.621,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[175.146,42.869,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[175.542,43.109,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[175.955,43.338,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[176.384,43.555,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[176.824,43.759,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.273,43.947,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[177.729,44.119,0],\"t\":58,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.189,44.273,0],\"t\":59,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[178.647,44.409,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.102,44.525,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.548,44.62,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[179.982,44.695,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.4,44.748,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[180.796,44.779,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.167,44.788,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.509,44.776,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[181.86,44.736,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.225,44.669,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.604,44.574,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[182.993,44.452,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.389,44.303,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[183.79,44.128,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.192,43.927,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.593,43.701,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[184.991,43.452,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.382,43.179,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[185.764,42.884,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.134,42.569,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.49,42.233,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[186.829,41.879,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.149,41.508,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.448,41.121,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.723,40.72,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[187.974,40.306,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[188.197,39.881,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[188.391,39.447,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[188.555,39.005,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[188.687,38.557,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[188.787,38.105,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"9\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0},{\"ddd\":0,\"ind\":84,\"ty\":4,\"nm\":\".dot3\",\"cl\":\"dot3\",\"parent\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"k\":[{\"s\":[452.57,29.124,0],\"t\":0,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.29,28.906,0],\"t\":1,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.038,28.659,0],\"t\":2,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.815,28.381,0],\"t\":3,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.621,28.075,0],\"t\":4,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.457,27.741,0],\"t\":5,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.323,27.38,0],\"t\":6,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.195,27.046,0],\"t\":7,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.059,26.776,0],\"t\":8,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.917,26.564,0],\"t\":9,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.773,26.408,0],\"t\":10,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.628,26.303,0],\"t\":11,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.484,26.245,0],\"t\":12,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.345,26.231,0],\"t\":13,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.211,26.258,0],\"t\":14,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.085,26.322,0],\"t\":15,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.967,26.419,0],\"t\":16,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.859,26.546,0],\"t\":17,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.761,26.701,0],\"t\":18,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.675,26.879,0],\"t\":19,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.601,27.078,0],\"t\":20,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.539,27.295,0],\"t\":21,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.489,27.527,0],\"t\":22,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.451,27.772,0],\"t\":23,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.425,28.027,0],\"t\":24,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.41,28.289,0],\"t\":25,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.405,28.555,0],\"t\":26,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.41,28.825,0],\"t\":27,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.423,29.095,0],\"t\":28,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.443,29.363,0],\"t\":29,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.467,29.628,0],\"t\":30,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.496,29.888,0],\"t\":31,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.526,30.141,0],\"t\":32,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.555,30.385,0],\"t\":33,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.58,30.619,0],\"t\":34,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.601,30.842,0],\"t\":35,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.612,31.053,0],\"t\":36,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.627,31.259,0],\"t\":37,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.652,31.466,0],\"t\":38,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.688,31.673,0],\"t\":39,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.735,31.878,0],\"t\":40,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.791,32.081,0],\"t\":41,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.858,32.281,0],\"t\":42,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[449.933,32.475,0],\"t\":43,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.017,32.664,0],\"t\":44,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.109,32.847,0],\"t\":45,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.208,33.021,0],\"t\":46,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.314,33.187,0],\"t\":47,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.426,33.343,0],\"t\":48,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.543,33.489,0],\"t\":49,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.665,33.623,0],\"t\":50,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.791,33.745,0],\"t\":51,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[450.92,33.854,0],\"t\":52,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.051,33.95,0],\"t\":53,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.184,34.031,0],\"t\":54,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.317,34.097,0],\"t\":55,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.451,34.147,0],\"t\":56,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.584,34.182,0],\"t\":57,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[451.97,34.186,0],\"t\":60,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.093,34.153,0],\"t\":61,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.211,34.104,0],\"t\":62,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.324,34.037,0],\"t\":63,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.432,33.954,0],\"t\":64,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.533,33.853,0],\"t\":65,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.628,33.737,0],\"t\":66,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.714,33.609,0],\"t\":67,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.791,33.475,0],\"t\":68,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.859,33.336,0],\"t\":69,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.916,33.192,0],\"t\":70,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.964,33.044,0],\"t\":71,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[453.003,32.892,0],\"t\":72,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[453.032,32.737,0],\"t\":73,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[453.053,32.578,0],\"t\":74,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[453.064,32.416,0],\"t\":75,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[453.068,32.25,0],\"t\":76,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[453.063,32.08,0],\"t\":77,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[453.051,31.905,0],\"t\":78,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[453.031,31.726,0],\"t\":79,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[453.006,31.54,0],\"t\":80,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.974,31.347,0],\"t\":81,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.938,31.147,0],\"t\":82,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.897,30.937,0],\"t\":83,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.853,30.718,0],\"t\":84,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.807,30.488,0],\"t\":85,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.759,30.246,0],\"t\":86,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.71,29.989,0],\"t\":87,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.662,29.718,0],\"t\":88,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.615,29.43,0],\"t\":89,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}},{\"s\":[452.57,29.124,0],\"t\":90,\"i\":{\"x\":1,\"y\":1},\"o\":{\"x\":0,\"y\":0}}],\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[6.829,6.829],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":2,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.117647059262,0.301960796118,0.321568638086,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[200,200],\"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\":\"1697\",\"np\":2,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0}]}],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":0,\"nm\":\"shapes\",\"refId\":\"comp_0\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[362,230,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[412,892,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6,\"l\":2}},\"ao\":0,\"ef\":[{\"ty\":5,\"nm\":\"Hue/Saturation\",\"np\":11,\"mn\":\"ADBE HUE SATURATION\",\"ix\":1,\"en\":1,\"ef\":[{\"ty\":7,\"nm\":\"Channel Control\",\"mn\":\"ADBE HUE SATURATION-0002\",\"ix\":1,\"v\":{\"a\":0,\"k\":1,\"ix\":1}},{},{\"ty\":0,\"nm\":\"Master Hue\",\"mn\":\"ADBE HUE SATURATION-0004\",\"ix\":3,\"v\":{\"a\":0,\"k\":0,\"ix\":3}},{\"ty\":0,\"nm\":\"Master Saturation\",\"mn\":\"ADBE HUE SATURATION-0005\",\"ix\":4,\"v\":{\"a\":0,\"k\":77,\"ix\":4}},{\"ty\":0,\"nm\":\"Master Lightness\",\"mn\":\"ADBE HUE SATURATION-0006\",\"ix\":5,\"v\":{\"a\":0,\"k\":51,\"ix\":5}},{\"ty\":7,\"nm\":\"Colorize\",\"mn\":\"ADBE HUE SATURATION-0007\",\"ix\":6,\"v\":{\"a\":0,\"k\":0,\"ix\":6}},{\"ty\":0,\"nm\":\"Colorize Hue\",\"mn\":\"ADBE HUE SATURATION-0008\",\"ix\":7,\"v\":{\"a\":0,\"k\":0,\"ix\":7}},{\"ty\":0,\"nm\":\"Colorize Saturation\",\"mn\":\"ADBE HUE SATURATION-0009\",\"ix\":8,\"v\":{\"a\":0,\"k\":25,\"ix\":8}},{\"ty\":0,\"nm\":\"Colorize Lightness\",\"mn\":\"ADBE HUE SATURATION-0010\",\"ix\":9,\"v\":{\"a\":0,\"k\":0,\"ix\":9}}]}],\"w\":824,\"h\":1784,\"ip\":0,\"op\":271,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\".bg\",\"cl\":\"bg\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[362,362,0],\"ix\":2,\"l\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1,\"l\":2},\"s\":{\"a\":0,\"k\":[200,200,100],\"ix\":6,\"l\":2}},\"ao\":0,\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ind\":0,\"ty\":\"sh\",\"ix\":1,\"ks\":{\"a\":0,\"k\":{\"i\":[[0,0],[-33.768,-77.755],[0,0],[10.315,-23.739],[0,0],[-77.754,33.772],[0,0],[-23.745,-10.309],[0,0],[33.768,77.755],[0,0],[-10.296,23.741],[0,0],[77.773,-33.77],[0,0],[23.725,10.311]],\"o\":[[-77.754,-33.77],[0,0],[10.315,23.741],[0,0],[-33.768,77.755],[0,0],[23.725,-10.309],[0,0],[77.773,33.772],[0,0],[-10.296,-23.739],[0,0],[33.768,-77.755],[0,0],[-23.745,10.311],[0,0]],\"v\":[[-50.069,-172.976],[-172.977,-50.071],[-167.391,-37.216],[-167.391,37.215],[-172.977,50.072],[-50.069,172.975],[-37.204,167.391],[37.222,167.391],[50.067,172.975],[172.975,50.072],[167.389,37.215],[167.389,-37.216],[172.975,-50.071],[50.067,-172.976],[37.222,-167.392],[-37.204,-167.392]],\"c\":true},\"ix\":2},\"nm\":\"Path 1\",\"mn\":\"ADBE Vector Shape - Group\",\"hd\":false},{\"ty\":\"rd\",\"nm\":\"Round Corners 1\",\"r\":{\"a\":0,\"k\":22,\"ix\":1},\"ix\":2,\"mn\":\"ADBE Vector Filter - RC\",\"hd\":false},{\"ty\":\"fl\",\"c\":{\"a\":0,\"k\":[0.152941182256,0.172549024224,0.149019613862,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\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":-90,\"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\":\"Base shape\",\"np\":3,\"cix\":2,\"bm\":0,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":900,\"st\":0,\"ct\":1,\"bm\":0}],\"markers\":[{\"tm\":90,\"cm\":\"Loop\",\"dr\":0},{\"tm\":270.5,\"cm\":\"End\",\"dr\":0}],\"props\":{}}"
  },
  {
    "path": "app/shared/src/main/res/values/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<resources>\n    <string-array name=\"pgc_index_filter_area_name\">\n        <item>全部</item>\n        <item>中国大陆</item>\n        <item>日本</item>\n        <item>美国</item>\n        <item>英国</item>\n        <item>其他</item>\n        <item>中国港台</item>\n        <item>韩国</item>\n        <item>法国</item>\n        <item>泰国</item>\n        <item>西班牙</item>\n        <item>德国</item>\n        <item>意大利</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_copyright_name\">\n        <item>全部</item>\n        <item>独家</item>\n        <item>其他</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_is_finish_name\">\n        <item>全部</item>\n        <item>完结</item>\n        <item>连载</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_order_name\">\n        <item>更新时间</item>\n        <item>弹幕数量</item>\n        <item>播放数量</item>\n        <item>追番人数</item>\n        <item>最高评分</item>\n        <item>开播时间</item>\n        <item>上映时间</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_order_type_name\">\n        <item>降序</item>\n        <item>升序</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_producer_name\">\n        <item>全部</item>\n        <item>BBC</item>\n        <item>NHK</item>\n        <item>SKY</item>\n        <item>央视</item>\n        <item>ITV</item>\n        <item>历史频道</item>\n        <item>探索频道</item>\n        <item>卫视</item>\n        <item>自制</item>\n        <item>ZDF</item>\n        <item>合作机构</item>\n        <item>国内其他</item>\n        <item>国外其他</item>\n        <item>国家地理</item>\n        <item>索尼</item>\n        <item>环球</item>\n        <item>派拉蒙</item>\n        <item>华纳</item>\n        <item>迪士尼</item>\n        <item>HBO</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_release_date_name\">\n        <item>全部</item>\n        <item>2026</item>\n        <item>2025</item>\n        <item>2024</item>\n        <item>2023</item>\n        <item>2022</item>\n        <item>2021</item>\n        <item>2020</item>\n        <item>2019</item>\n        <item>2018</item>\n        <item>2017</item>\n        <item>2016</item>\n        <item>2015-2010</item>\n        <item>2009-2005</item>\n        <item>2004-2000</item>\n        <item>90年代</item>\n        <item>80年代</item>\n        <item>更早</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_season_month_name\">\n        <item>全部</item>\n        <item>1月</item>\n        <item>4月</item>\n        <item>7月</item>\n        <item>10月</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_season_status_name\">\n        <item>全部</item>\n        <item>免费</item>\n        <item>付费</item>\n        <item>大会员</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_season_version_name\">\n        <item>全部</item>\n        <item>正片</item>\n        <item>电影</item>\n        <item>其他</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_spoken_language_name\">\n        <item>全部</item>\n        <item>原声</item>\n        <item>中文配音</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_style_name\">\n        <item>全部</item>\n        <item>电影</item>\n        <item>原创</item>\n        <item>漫画改</item>\n        <item>小说改</item>\n        <item>游戏改</item>\n        <item>动态漫</item>\n        <item>布袋戏</item>\n        <item>热血</item>\n        <item>穿越</item>\n        <item>奇幻</item>\n        <item>玄幻</item>\n        <item>战斗</item>\n        <item>搞笑</item>\n        <item>日常</item>\n        <item>科幻</item>\n        <item>萌系</item>\n        <item>治愈</item>\n        <item>校园</item>\n        <item>少儿</item>\n        <item>泡面</item>\n        <item>恋爱</item>\n        <item>少女</item>\n        <item>魔法</item>\n        <item>冒险</item>\n        <item>历史</item>\n        <item>架空</item>\n        <item>机战</item>\n        <item>神魔</item>\n        <item>声控</item>\n        <item>运动</item>\n        <item>励志</item>\n        <item>音乐</item>\n        <item>推理</item>\n        <item>社团</item>\n        <item>智斗</item>\n        <item>催泪</item>\n        <item>美食</item>\n        <item>偶像</item>\n        <item>乙女</item>\n        <item>职场</item>\n        <item>古风</item>\n        <item>剧情</item>\n        <item>喜剧</item>\n        <item>爱情</item>\n        <item>动作</item>\n        <item>恐怖</item>\n        <item>犯罪</item>\n        <item>惊悚</item>\n        <item>悬疑</item>\n        <item>战争</item>\n        <!--<item>10059</item>-->\n        <item>传记</item>\n        <item>家庭</item>\n        <item>歌剧</item>\n        <item>纪实</item>\n        <item>灾难</item>\n        <item>人文</item>\n        <item>科技</item>\n        <item>探险</item>\n        <item>通用</item>\n        <item>萌宠</item>\n        <item>社会</item>\n        <item>动物</item>\n        <item>自然</item>\n        <item>医疗</item>\n        <item>军事</item>\n        <item>罪案</item>\n        <item>神秘</item>\n        <item>旅行</item>\n        <item>武侠</item>\n        <item>青春</item>\n        <item>都市</item>\n        <item>古装</item>\n        <item>谍战</item>\n        <item>经典</item>\n        <item>情感</item>\n        <item>神话</item>\n        <item>年代</item>\n        <item>农村</item>\n        <item>刑侦</item>\n        <item>军旅</item>\n        <item>访谈</item>\n        <item>脱口秀</item>\n        <item>真人秀</item>\n        <!-- <item>10093</item>-->\n        <item>选秀</item>\n        <item>旅游</item>\n        <item>演唱会</item>\n        <item>亲子</item>\n        <item>晚会</item>\n        <item>养成</item>\n        <item>文化</item>\n        <!--<item>10101</item>-->\n        <item>特摄</item>\n        <item>短剧</item>\n        <item>短片</item>\n    </string-array>\n    <string-array name=\"pgc_index_filter_year_name\">\n        <item>全部</item>\n        <item>2026</item>\n        <item>2025</item>\n        <item>2024</item>\n        <item>2023</item>\n        <item>2022</item>\n        <item>2021</item>\n        <item>2020</item>\n        <item>2019</item>\n        <item>2018</item>\n        <item>2017</item>\n        <item>2016</item>\n        <item>2015</item>\n        <item>2014-2010</item>\n        <item>2009-2005</item>\n        <item>2004-2000</item>\n        <item>90年代</item>\n        <item>80年代</item>\n        <item>更早</item>\n    </string-array>\n\n    <string-array name=\"user_homepage_random_title\">\n        <item>吾</item>\n        <item>熟悉的屏幕</item>\n        <item>你来辣</item>\n        <item>I Need More Power!!!</item>\n        <item>别看了</item>\n        <item>我的</item>\n        <item>BUG 满天飞 ~</item>\n        <item>你说得对，但是</item>\n        <item>让我康康</item>\n    </string-array>\n</resources>\n"
  },
  {
    "path": "app/shared/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    <color name=\"ic_banner_background\">#FFFFFF</color>\n</resources>"
  },
  {
    "path": "app/shared/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"anime_home_button_following\">正在追</string>\n    <string name=\"anime_home_button_gamer_ani\">動畫風</string>\n    <string name=\"anime_home_button_gamer_ani_launch_failed\">未安装動畫風</string>\n    <string name=\"anime_home_button_index\">索引</string>\n    <string name=\"anime_home_button_timeline\">时间表</string>\n\n    <string name=\"app_name\">BV</string>\n\n    <string name=\"blacklist_user_toast\">您已被开发者拉黑</string>\n\n    <string name=\"codec_detail_audio_bitrate_range_title\">音频码率范围</string>\n    <string name=\"codec_detail_color_formats_title\">颜色格式</string>\n    <string name=\"codec_detail_hs_hardware\">硬解</string>\n    <string name=\"codec_detail_hs_software\">软解</string>\n    <string name=\"codec_detail_hs_title\">软硬解码</string>\n    <string name=\"codec_detail_max_supported_instances_title\">最大并发编解码器实例数量</string>\n    <string name=\"codec_detail_video_frame_achievable_title\">可实现的视频帧率（可能无数据）</string>\n    <string name=\"codec_detail_video_frame_range_title\">视频帧率范围</string>\n    <string name=\"codec_detail_video_frame_supported_title\">支持的视频帧率</string>\n    <string name=\"codec_detail_video_frame_unsupported\">不受支持</string>\n    <string name=\"codec_detail_video_max_bitrate_title\">最大视频码率</string>\n    <string name=\"codec_detail_video_resolution_1080p\">1080P</string>\n    <string name=\"codec_detail_video_resolution_1440p\">2K</string>\n    <string name=\"codec_detail_video_resolution_2160p\">4K</string>\n    <string name=\"codec_detail_video_resolution_360p\">360P</string>\n    <string name=\"codec_detail_video_resolution_4320p\">8K</string>\n    <string name=\"codec_detail_video_resolution_480p\">480P</string>\n    <string name=\"codec_detail_video_resolution_720p\">720P</string>\n    <string name=\"codec_detail_video_resolution_unknown\">Unknown</string>\n    <string name=\"codec_list_empty\">未找到可用解码器</string>\n\n    <string name=\"common_cancel\">取消</string>\n    <string name=\"common_confirm\">确定</string>\n\n    <string name=\"confirm\">确认</string>\n\n    <string name=\"view_at\">看</string>\n    <string name=\"favorite_at\">藏</string>\n\n    <string name=\"date_format_hours_age\">%1$d 小时前</string>\n    <string name=\"date_format_just_now\">刚刚</string>\n    <string name=\"date_format_minutes_age\">%1$d 分钟前</string>\n\n    <string name=\"delete_account_confirm_dialog_confirm\">gkd 别墨迹</string>\n    <string name=\"delete_account_confirm_dialog_dismiss\">不，我不想</string>\n    <string name=\"delete_account_confirm_dialog_text\">是否移除账号 %1$s\\nUID:%2$d</string>\n    <string name=\"delete_account_confirm_dialog_title\">移除账号</string>\n\n    <string name=\"exception_auth_failure\">用户身份认证已失效，请重新登录</string>\n\n    <string name=\"like_button_text\">点赞</string>\n    <string name=\"coin_button_text\">投币</string>\n    <string name=\"toview_button_text\">稍后再看</string>\n\n    <string name=\"favorite_button_text\">收藏</string>\n    <string name=\"favorite_dialog_title\">选择收藏夹</string>\n\n    <string name=\"filter_dialog_open_tip\">在列表区域长按确认键更改筛选</string>\n    <string name=\"filter_dialog_open_tip_click\">点击打开筛选</string>\n    <string name=\"filter_dialog_reset\">重置筛选</string>\n    <string name=\"filter_dialog_title\">筛选</string>\n\n    <string name=\"follow_bangumi_disable_fail\">取消追番失败</string>\n    <string name=\"follow_bangumi_enable_fail\">追番失败</string>\n\n    <string name=\"following_season_status_all\">全部</string>\n    <string name=\"following_season_status_want\">想看</string>\n    <string name=\"following_season_status_watched\">看过</string>\n    <string name=\"following_season_status_watching\">在看</string>\n    <string name=\"following_season_type_bangumi\">番剧</string>\n    <string name=\"following_season_type_film_and_television\">影视</string>\n\n    <string name=\"home_press_back_again_to_exit\">再次按下返回键退出 Bug Video</string>\n    <string name=\"home_tab_popular\">热门</string>\n    <string name=\"home_tab_rcmd\">推荐</string>\n\n    <string name=\"load_data_count\">已加载 %1$d 条数据</string>\n    <string name=\"load_data_count_no_more\">已加载全部共 %1$d 条数据</string>\n    <string name=\"load_data_no_more\">就只有这么多了</string>\n\n    <string name=\"loading\" tools:ignore=\"TypographyEllipsis\">加载中...</string>\n\n    <string name=\"log_list_empty\">无日志</string>\n    <string name=\"log_qr_code_empty\">请选择左侧日志文件</string>\n    <string name=\"log_save_now_button\">手动保存日志</string>\n    <string name=\"log_type_crash\">崩溃</string>\n    <string name=\"log_type_manual\">手动</string>\n\n    <string name=\"login_error\">出现错误</string>\n    <string name=\"login_expired\">二维码已过期</string>\n    <string name=\"login_requesting\">请求二维码中</string>\n    <string name=\"login_retry\">按下 确认键 重试</string>\n    <string name=\"login_success\">登录成功</string>\n    <string name=\"login_wait_for_confirm\">请确认登录</string>\n    <string name=\"login_wait_for_scan\">请扫描二维码</string>\n\n    <string name=\"logout_dialog_confirm\">gkd 别墨迹</string>\n    <string name=\"logout_dialog_dismiss\">不，我不想</string>\n    <string name=\"logout_dialog_text\">是否登出账号 %1$s</string>\n    <string name=\"logout_dialog_title\">登出确认</string>\n\n    <string name=\"no_data\">暂无数据</string>\n\n    <string name=\"pgc_home_button_unknown\">占位</string>\n    <string name=\"pgc_index_filter_area\">地区</string>\n    <string name=\"pgc_index_filter_copyright\">版权</string>\n    <string name=\"pgc_index_filter_is_finish\">状态</string>\n    <string name=\"pgc_index_filter_order\">排序方式</string>\n    <string name=\"pgc_index_filter_order_type\">排序顺序</string>\n    <string name=\"pgc_index_filter_producer\">出品</string>\n    <string name=\"pgc_index_filter_release_date\">年份</string>\n    <string name=\"pgc_index_filter_season_month\">季度</string>\n    <string name=\"pgc_index_filter_season_status\">付费</string>\n    <string name=\"pgc_index_filter_season_version\">类型</string>\n    <string name=\"pgc_index_filter_spoken_language\">配音</string>\n    <string name=\"pgc_index_filter_style\">风格</string>\n    <string name=\"pgc_index_filter_title_prefix\">\"索引筛选 - \"</string>\n    <string name=\"pgc_index_filter_year\">年份</string>\n    <string name=\"pgc_type_anime\">番剧</string>\n    <string name=\"pgc_type_documentary\">纪录片</string>\n    <string name=\"pgc_type_guochuang\">国创</string>\n    <string name=\"pgc_type_movie\">电影</string>\n    <string name=\"pgc_type_tv\">电视剧</string>\n    <string name=\"pgc_type_variety\">综艺</string>\n\n    <string name=\"play_time_finish\">已看完</string>\n    <string name=\"play_time_history\">%1$s / %2$s</string>\n\n    <string name=\"player_controller_menu_danmaku_disabled\">关闭</string>\n    <string name=\"player_controller_menu_danmaku_enabled\">开启</string>\n    <string name=\"player_controller_menu_danmaku_webmark_disabled\">关闭</string>\n    <string name=\"player_controller_menu_danmaku_webmark_enabled\">开启</string>\n    <string name=\"player_controller_menu_item_dankamu_size\">弹幕大小</string>\n    <string name=\"player_controller_menu_item_danmaku_area\">弹幕区域</string>\n    <string name=\"player_controller_menu_item_danmaku_switch\">弹幕开关</string>\n    <string name=\"player_controller_menu_item_danmaku_transparency\">弹幕透明</string>\n    <string name=\"player_controller_menu_item_resolution\">分辨率</string>\n    <string name=\"player_controller_menu_item_subtitle\">CC 字幕</string>\n    <string name=\"player_controller_menu_item_subtitle_font_size\">字幕大小</string>\n    <string name=\"player_controller_menu_item_subtitle_padding\">字幕底部间距</string>\n    <string name=\"player_controller_menu_item_video_aspect_ratio\">视频比例</string>\n    <string name=\"player_controller_menu_item_video_codec\">视频编码</string>\n    <string name=\"player_tip_need_pay\">请先购买影片再进行观看\\n不支持试看</string>\n\n    <string name=\"proxy_server_edit_dialog_input_field_label\">服务器域名</string>\n    <string name=\"proxy_server_edit_dialog_warning\">请谨慎使用代理服务器，因为您的登录凭证将会发送至代理服务器，使用代理服务器所带来的一切后果均由用户自行承担</string>\n\n    <string name=\"region_block_character_painting\">:(</string>\n    <string name=\"region_block_exit_button\">退出应用</string>\n    <string name=\"region_block_qr_content\">扫码也没有用的！</string>\n    <string name=\"region_block_solution_text\">不要在中国大陆地区使用该应用</string>\n    <string name=\"region_block_solution_title\">该问题的解决方法就是：</string>\n    <string name=\"region_block_subtitle_mobile\">Bug Video 无法在中国大陆地区使用</string>\n    <string name=\"region_block_subtitle_tv\">Bug Video 无法在中国大陆地区使用，您可以按任意键退出应用</string>\n    <string name=\"region_block_title\">不支持的使用地区</string>\n\n    <string name=\"remote_control_panel_demo_tip_back\">退出</string>\n    <string name=\"remote_control_panel_demo_tip_bottom\">按下 [CENTER] 键以继续</string>\n    <string name=\"remote_control_panel_demo_tip_center\">【短按】播放/暂停\\n【长按】打开菜单</string>\n    <string name=\"remote_control_panel_demo_tip_down\">【短按】视频播放信息\\n【双击】打开视频推荐列表</string>\n    <string name=\"remote_control_panel_demo_tip_lr\">后退/前进</string>\n    <string name=\"remote_control_panel_demo_tip_up\">剧集/合集/分 P 列表</string>\n\n    <string name=\"search_input_history\">历史记录</string>\n    <string name=\"search_input_history_delete_all_confirm_dialog_cancel_button\">手滑了</string>\n    <string name=\"search_input_history_delete_all_confirm_dialog_confirm_button\">确认删除</string>\n    <string name=\"search_input_history_delete_all_confirm_dialog_text\">是否删除所有历史记录，该操作无法撤销</string>\n    <string name=\"search_input_history_delete_all_confirm_dialog_title\">删除所有记录</string>\n    <string name=\"search_input_hotword\">bilibili 热搜</string>\n    <string name=\"search_input_soft_keybord_clear\">清空</string>\n    <string name=\"search_input_soft_keybord_delete\">删除</string>\n    <string name=\"search_input_soft_keybord_search\">搜索</string>\n    <string name=\"search_input_suggest\">你可能要找的是</string>\n    <string name=\"search_input_title\">搜索</string>\n    <string name=\"search_result_filter_duration_10_to_30\">10–30 分钟</string>\n    <string name=\"search_result_filter_duration_30_to_60\">30–60 分钟</string>\n    <string name=\"search_result_filter_duration_all\">全部时长</string>\n    <string name=\"search_result_filter_duration_less_than_10\">10 分钟以下</string>\n    <string name=\"search_result_filter_duration_more_than_60\">60 分钟以上</string>\n    <string name=\"search_result_filter_order_type_comprehensive_sort\">综合排序</string>\n    <string name=\"search_result_filter_order_type_latest_publish\">最新发布</string>\n    <string name=\"search_result_filter_order_type_most_clicks\">最多点击</string>\n    <string name=\"search_result_filter_order_type_most_danmaku\">最多弹幕</string>\n    <string name=\"search_result_filter_order_type_most_favorites\">最多收藏</string>\n    <string name=\"search_result_type_name_live_room\">直播</string>\n    <string name=\"search_result_type_name_bili_user\">用户</string>\n    <string name=\"search_result_type_name_media_bangumi\">番剧</string>\n    <string name=\"search_result_type_name_media_ft\">影视</string>\n    <string name=\"search_result_type_name_video\">视频</string>\n\n    <string name=\"season_count_tip\">系列共 %1$s 部</string>\n    <string name=\"season_feature_film\">正片</string>\n    <string name=\"season_no_feature_film\">还没有正片</string>\n\n    <string name=\"settings_crash_test_text\">点一下崩一下，崩溃不花一份钱</string>\n    <string name=\"settings_crash_test_title\">崩溃测试</string>\n    <string name=\"settings_create_logs_text\">查看最近的崩溃日志，也可以手动保存日志</string>\n    <string name=\"settings_create_logs_title\">查看日志</string>\n    <string name=\"settings_info_manufacturer\">厂商: %1$s</string>\n    <string name=\"settings_info_memory\">内存: 可用 %1$s 共 %2$s</string>\n    <string name=\"settings_info_model\">型号: %1$s (%2$s)</string>\n    <string name=\"settings_info_screen\">屏幕: %1$d x %2$d %3$fHz</string>\n    <string name=\"settings_info_soc\">SOC: %1$s %2$s</string>\n    <string name=\"settings_info_storage\">存储: 可用 %1$s 共 %2$s</string>\n    <string name=\"settings_info_system\">系统: Android %1$s</string>\n    <string name=\"settings_item_about\">关于</string>\n    <string name=\"settings_item_api\">接口偏好</string>\n    <string name=\"settings_item_audio\">默认音频编码</string>\n    <string name=\"settings_item_codec\">默认视频编码</string>\n    <string name=\"settings_item_live_codec\">默认直播编码</string>\n    <string name=\"settings_item_info\">设备信息</string>\n    <string name=\"settings_item_live\">直播</string>\n    <string name=\"settings_item_network\">网络设置</string>\n    <string name=\"settings_item_other\">更多设置</string>\n    <string name=\"settings_item_player_type\">播放器内核</string>\n    <string name=\"settings_item_resolution\">默认分辨率</string>\n    <string name=\"settings_item_storage\">存储管理</string>\n    <string name=\"settings_item_ui\">界面设置</string>\n    <string name=\"settings_item_player\">播放设置</string>\n    <string name=\"settings_item_danmaku_filter\">弹幕过滤</string>\n    <string name=\"settings_player_danmaku_filter_level_title\">视频弹幕过滤等级</string>\n    <string name=\"settings_player_danmaku_filter_level_text\">过滤等级低于设定值的弹幕（0-10，0为显示全部）</string>\n    <string name=\"settings_live_danmaku_filter_level_title\">直播弹幕过滤等级</string>\n    <string name=\"settings_live_danmaku_filter_level_text\">过滤用户等级低于设定值的用户发的弹幕（0-60，0为显示全部）</string>\n    <string name=\"settings_network_enable_proxy_text\">使用自定义代理服务器获取部分视频播放地址</string>\n    <string name=\"settings_network_enable_proxy_title\">启用代理</string>\n    <string name=\"settings_network_prefer_official_cdn_text\">优先使用获取到的官方 CDN</string>\n    <string name=\"settings_network_prefer_official_cdn_title\">P(M)CDN 快走开</string>\n    <string name=\"settings_network_ipv4_only_title\">仅使用 IPv4</string>\n    <string name=\"settings_network_ipv4_only_text\">禁用 IPv6 地址解析，仅通过 IPv4 连接网络</string>\n    <string name=\"settings_network_proxy_grpc_server_title\">gRPC 接口代理服务器</string>\n    <string name=\"settings_network_proxy_http_server_title\">HTTP 接口代理服务器</string>\n    <string name=\"settings_network_proxy_server_content_empty\">未填写</string>\n    <string name=\"settings_network_test_text\">使用最后一次播放的视频进行网络测试</string>\n    <string name=\"settings_network_test_title\">网络测试</string>\n    <string name=\"settings_other_alpha_text\">获取最新自动构建的 Alpha 版更新</string>\n    <string name=\"settings_other_alpha_title\">Alpha 版更新</string>\n    <string name=\"settings_other_ffmpeg_audio_renderer_text\">尝试使用 FFmpeg 播放硬解不兼容的音频</string>\n    <string name=\"settings_other_ffmpeg_audio_renderer_title\">启用音频软解</string>\n    <string name=\"settings_other_fps_text\">在屏幕左上角显示 FPS，这需要重启 App</string>\n    <string name=\"settings_other_fps_title\">FPS 显示</string>\n    <string name=\"settings_storage_calculating\">计算中</string>\n    <string name=\"settings_storage_crash_logs\">崩溃日志</string>\n    <string name=\"settings_storage_image_cache\">图片缓存</string>\n    <string name=\"settings_storage_libvlc_files\">LibVLC 文件</string>\n    <string name=\"settings_storage_others_cache\">其他缓存</string>\n    <string name=\"settings_ui_density_text\">更改整个应用的大小缩放</string>\n    <string name=\"settings_ui_density_title\">界面缩放</string>\n    <string name=\"settings_ui_interface_mode_text\">设置启动时自动检测、强制使用 TV，或强制使用 Mobile 界面</string>\n    <string name=\"settings_ui_interface_mode_title\">界面模式</string>\n    <string name=\"settings_ui_theme_type_text\">更改主题样式</string>\n    <string name=\"settings_ui_theme_type_title\">主题</string>\n    <string name=\"settings_ui_ugc_video_info_history_count_title\">UGC 视频详情页面的历史记录数量</string>\n    <string name=\"settings_ui_ugc_video_info_history_count_text\">设置可保留历史记录的 UGC 视频详情页数量</string>\n    <string name=\"settings_ui_video_info_history_include_from_player_title\">详情页历史记录是否包含播放器打开的详情页</string>\n    <string name=\"settings_ui_video_info_history_include_from_player_text\">关闭时，从播放器页面隐式打开的视频详情历史记录不保留</string>\n    <string name=\"settings_ui_ugc_video_player_history_count_title\">UGC 视频播放页面的历史记录数量</string>\n    <string name=\"settings_ui_ugc_video_player_history_count_text\">设置可保留历史记录的 UGC 视频播放页数量</string>\n    <string name=\"settings_ui_home_nav_items_title\">首页导航项</string>\n    <string name=\"settings_ui_home_nav_items_text\">设置首页顶部导航项的显示顺序、隐藏状态、默认选中项（也是app启动时的初始页面）</string>\n    <string name=\"settings_ui_home_nav_items_hint\">左右键排序 · 短按确认键显示/隐藏 · 长按确认键设为默认</string>\n    <string name=\"settings_ui_home_nav_default_tag\">默认</string>\n    <string name=\"settings_ui_home_nav_visible\">显示</string>\n    <string name=\"settings_ui_home_nav_hidden\">隐藏</string>\n    <string name=\"settings_ui_ugc_nav_items_title\">UGC 导航项</string>\n    <string name=\"settings_ui_ugc_nav_items_text\">设置 UGC 页顶部导航项的显示顺序和隐藏状态</string>\n    <string name=\"settings_ui_ugc_nav_items_hint\">使用左右键移动顺序，按确认键切换显示/隐藏</string>\n    <string name=\"settings_ui_pgc_nav_items_title\">PGC 导航项</string>\n    <string name=\"settings_ui_pgc_nav_items_text\">设置 PGC 页顶部导航项的显示顺序和隐藏状态</string>\n    <string name=\"settings_ui_pgc_nav_items_hint\">使用左右键移动顺序，按确认键切换显示/隐藏</string>\n    <string name=\"settings_ui_live_nav_items_title\">直播导航项</string>\n    <string name=\"settings_ui_live_nav_items_text\">设置直播页顶部导航项的显示顺序和隐藏状态</string>\n    <string name=\"settings_ui_live_nav_items_hint\">使用左右键移动顺序，按确认键切换显示/隐藏</string>\n    <string name=\"settings_ui_live_nav_items_empty_hint\">请先访问直播页以加载分区数据</string>\n    <string name=\"settings_ui_drawer_nav_items_title\">主导航项</string>\n    <string name=\"settings_ui_drawer_nav_items_text\">设置左侧导航栏的显示顺序和隐藏状态</string>\n    <string name=\"settings_ui_drawer_nav_items_hint\">使用左右键移动顺序，按确认键切换显示/隐藏</string>\n    <string name=\"settings_live_enable\">显示直播</string>\n    <string name=\"settings_live_enable_desc\">在侧边栏显示直播入口</string>\n    <string name=\"settings_version_check_update_button\">去更新</string>\n    <string name=\"settings_version_current_version\">当前版本: %1$s</string>\n    <string name=\"settings_version_latest_version\">最新版本: %1$s</string>\n\n    <string name=\"sms_login_button_login\">登录</string>\n    <string name=\"sms_login_button_send_sms\">发送短信</string>\n    <string name=\"sms_login_code\">验证码</string>\n    <string name=\"sms_login_phone_number\">手机号</string>\n    <string name=\"sms_login_toast_send_sms_first\">请先发送验证码</string>\n\n    <string name=\"theme_type_auto\">跟随系统</string>\n    <string name=\"theme_type_dark\">深色</string>\n    <string name=\"theme_type_light\">浅色</string>\n    <string name=\"interface_mode_auto\">自动</string>\n    <string name=\"interface_mode_tv\">TV</string>\n    <string name=\"interface_mode_mobile\">Mobile</string>\n    <string name=\"nav_switch_mode_auto\">自动切换</string>\n    <string name=\"nav_switch_mode_confirm\">按确认键切换</string>\n    <string name=\"settings_ui_nav_switch_mode_title\">导航切换模式</string>\n    <string name=\"settings_ui_nav_switch_mode_text\">设置左侧菜单和顶部标签的切换方式</string>\n\n    <string name=\"title_activity_anime_timeline\">番剧放送时间表</string>\n    <string name=\"title_activity_favorite\">个人收藏</string>\n    <string name=\"title_activity_follow\">已关注</string>\n    <string name=\"title_activity_following_season\">正在追</string>\n    <string name=\"title_activity_history\">历史记录</string>\n    <string name=\"title_activity_login\">登录</string>\n    <string name=\"title_activity_logs\">日志列表</string>\n    <string name=\"title_activity_media_codec\">解码器信息</string>\n    <string name=\"title_activity_pgc_index\">PGC 索引</string>\n    <string name=\"title_activity_remote_controller_panel_demo\">遥控板按键演示</string>\n    <string name=\"title_activity_search_input\">搜索输入</string>\n    <string name=\"title_activity_search_result\">搜索结果</string>\n    <string name=\"title_activity_season_info\">剧集信息</string>\n    <string name=\"title_activity_settings\">设置</string>\n    <string name=\"title_activity_speed_test\">网络测速</string>\n    <string name=\"title_activity_tag\">视频标签</string>\n    <string name=\"title_activity_toview\">现在不看</string>\n    <string name=\"title_activity_up_info\">UP 投稿</string>\n    <string name=\"toview_delete_confirm_dialog_title\">删除稍后再看</string>\n    <string name=\"toview_delete_confirm_dialog_text\">确认删除「%1$s」吗？</string>\n    <string name=\"toview_delete_confirm_dialog_confirm\">确认</string>\n    <string name=\"toview_delete_confirm_dialog_dismiss\">取消</string>\n    <string name=\"toview_delete_success\">删除成功</string>\n    <string name=\"toview_delete_failed\">删除失败</string>\n    <string name=\"toview_clear_confirm_dialog_title\">清空稍后再看</string>\n    <string name=\"toview_clear_confirm_dialog_text\">确认要清空列表吗？</string>\n    <string name=\"toview_clear_success\">清空成功</string>\n    <string name=\"toview_clear_failed\">清空失败</string>\n    <string name=\"history_delete_confirm_dialog_title\">删除历史记录</string>\n    <string name=\"history_delete_confirm_dialog_text\">确认删除「%1$s」吗？</string>\n    <string name=\"history_delete_confirm_dialog_confirm\">确认</string>\n    <string name=\"history_delete_confirm_dialog_dismiss\">取消</string>\n    <string name=\"history_delete_success\">删除成功</string>\n    <string name=\"history_delete_failed\">删除失败</string>\n    <string name=\"history_clear_confirm_dialog_title\">清空历史记录</string>\n    <string name=\"history_clear_confirm_dialog_text\">确认要清空列表吗？</string>\n    <string name=\"history_clear_success\">清空成功</string>\n    <string name=\"history_clear_failed\">清空失败</string>\n    <string name=\"favorite_delete_confirm_dialog_title\">取消收藏</string>\n    <string name=\"favorite_delete_confirm_dialog_text\">确认取消收藏「%1$s」吗？</string>\n    <string name=\"favorite_delete_confirm_dialog_confirm\">确认</string>\n    <string name=\"favorite_delete_confirm_dialog_dismiss\">取消</string>\n    <string name=\"favorite_delete_success\">取消收藏成功</string>\n    <string name=\"favorite_delete_failed\">取消收藏失败</string>\n    <string name=\"following_season_delete_confirm_dialog_title\">取消追番</string>\n    <string name=\"following_season_delete_confirm_dialog_text\">确认取消追番「%1$s」吗？</string>\n    <string name=\"following_season_delete_confirm_dialog_confirm\">确认</string>\n    <string name=\"following_season_delete_confirm_dialog_dismiss\">取消</string>\n    <string name=\"following_season_delete_success\">取消追番成功</string>\n    <string name=\"following_season_delete_failed\">取消追番失败</string>\n    <string name=\"delete_mode_hint\">在列表区域按菜单键进入删除模式</string>\n    <string name=\"delete_mode_action_hint\">长按（或短按）确认键删除当前选中项，按返回键退出删除模式</string>\n    <string name=\"following_season_hint\">在列表区域长按确认键更改筛选，按菜单键进入删除模式</string>\n    <string name=\"title_activity_user_info\">用户信息</string>\n    <string name=\"title_activity_user_lock_settings\">用户锁设置</string>\n    <string name=\"title_activity_user_switch\">用户切换</string>\n    <string name=\"title_activity_video_info\">视频信息</string>\n    <string name=\"title_activity_video_player_v3\">视频播放</string>\n    <string name=\"title_mobile_activity_dynamic_detail\">动态详情</string>\n    <string name=\"title_mobile_activity_favorite\">我的收藏</string>\n    <string name=\"title_mobile_activity_following_season\">我的追番</string>\n    <string name=\"title_mobile_activity_following_user\">我的关注</string>\n    <string name=\"title_mobile_activity_history\">历史记录</string>\n    <string name=\"title_mobile_activity_login\">用户登录</string>\n    <string name=\"title_mobile_activity_settings\">设置</string>\n    <string name=\"title_mobile_activity_user_space\">用户空间</string>\n    <string name=\"title_mobile_activity_video_player\">视频播放</string>\n\n    <string name=\"top_nav_item_anime\">番剧</string>\n    <string name=\"top_nav_item_dynamics\">动态</string>\n    <string name=\"top_nav_item_partition\">分区</string>\n    <string name=\"top_nav_item_popular\">热门</string>\n    <string name=\"top_nav_item_recommend\">推荐</string>\n    <string name=\"top_nav_item_search\">搜索</string>\n\n    <string name=\"ugc_type_animal\">动物圈</string>\n    <string name=\"ugc_type_animal_cat\">喵星人</string>\n    <string name=\"ugc_type_animal_composite\">动物综合</string>\n    <string name=\"ugc_type_animal_dog\">汪星人</string>\n    <string name=\"ugc_type_animal_reptiles\">小宠异宠</string>\n    <string name=\"ugc_type_animal_second_edition\">动物二创</string>\n    <string name=\"ugc_type_animal_wild_anima\">野生动物</string>\n    <string name=\"ugc_type_car\">汽车</string>\n    <string name=\"ugc_type_car_knowledge\">汽车知识科普</string>\n    <string name=\"ugc_type_car_life\">汽车生活</string>\n    <string name=\"ugc_type_car_modified_vehicle\">改装玩车</string>\n    <string name=\"ugc_type_car_motorcycle\">摩托车</string>\n    <string name=\"ugc_type_car_new_energy_vehicle\">新能源车</string>\n    <string name=\"ugc_type_car_racing\">赛车</string>\n    <string name=\"ugc_type_car_strategy\">购车攻略</string>\n    <string name=\"ugc_type_car_touring_car\">房车</string>\n    <string name=\"ugc_type_cinephile\">影视</string>\n    <string name=\"ugc_type_cinephile_ai_imagine\">AI影像</string>\n    <string name=\"ugc_type_cinephile_cinecism\">影视杂谈</string>\n    <string name=\"ugc_type_cinephile_comperhensive\">影视综合</string>\n    <string name=\"ugc_type_cinephile_mashup\">影视整活</string>\n    <string name=\"ugc_type_cinephile_nibtage\">影视剪辑</string>\n    <string name=\"ugc_type_cinephile_short_film\">短片</string>\n    <string name=\"ugc_type_cinephile_short_play\">小剧场</string>\n    <string name=\"ugc_type_cinephile_trailer_info\">预告·资讯</string>\n    <string name=\"ugc_type_dance\">舞蹈</string>\n    <string name=\"ugc_type_dance_china\">国风舞蹈</string>\n    <string name=\"ugc_type_dance_demo\">舞蹈教程</string>\n    <string name=\"ugc_type_dance_gestures\">颜值·网红舞</string>\n    <string name=\"ugc_type_dance_hiphop\">街舞</string>\n    <string name=\"ugc_type_dance_otaku\">宅舞</string>\n    <string name=\"ugc_type_dance_star\">明星舞蹈</string>\n    <string name=\"ugc_type_dance_three_d\">舞蹈综合</string>\n    <string name=\"ugc_type_douga\">动画</string>\n    <string name=\"ugc_type_douga_acgn_talks\">动漫杂谈</string>\n    <string name=\"ugc_type_douga_garage_kit\">手办·模玩</string>\n    <string name=\"ugc_type_douga_hand_drawn\">同人·手书</string>\n    <string name=\"ugc_type_douga_mad\">MAD·AMV</string>\n    <string name=\"ugc_type_douga_mmd\">MMD·3D</string>\n    <string name=\"ugc_type_douga_other\">综合</string>\n    <string name=\"ugc_type_douga_tokusatsu\">特摄</string>\n    <string name=\"ugc_type_douga_voice\">配音</string>\n    <string name=\"ugc_type_ent\">娱乐</string>\n    <string name=\"ugc_type_ent_beauty\">颜值安利</string>\n    <string name=\"ugc_type_ent_celebrity\">明星综合</string>\n    <string name=\"ugc_type_ent_cp_recommendation\">CP安利</string>\n    <string name=\"ugc_type_ent_entertainment_news\">娱乐资讯</string>\n    <string name=\"ugc_type_ent_fans\">娱乐粉丝创作</string>\n    <string name=\"ugc_type_ent_talker\">娱乐杂谈</string>\n    <string name=\"ugc_type_ent_variety\">综艺</string>\n    <string name=\"ugc_type_fashion\">时尚</string>\n    <string name=\"ugc_type_fashion_catwalk\">时尚潮流</string>\n    <string name=\"ugc_type_fashion_clothing\">穿搭</string>\n    <string name=\"ugc_type_fashion_cos\">仿妆cos</string>\n    <string name=\"ugc_type_fashion_makeup\">美妆护肤</string>\n    <string name=\"ugc_type_food\">美食</string>\n    <string name=\"ugc_type_food_detective\">美食侦探</string>\n    <string name=\"ugc_type_food_make\">美食制作</string>\n    <string name=\"ugc_type_food_measurement\">美食测评</string>\n    <string name=\"ugc_type_food_record\">美食记录</string>\n    <string name=\"ugc_type_food_rural\">田园美食</string>\n    <string name=\"ugc_type_game\">游戏</string>\n    <string name=\"ugc_type_game_board\">桌游棋牌</string>\n    <string name=\"ugc_type_game_e_sports\">电子竞技</string>\n    <string name=\"ugc_type_game_gmv\">GMV</string>\n    <string name=\"ugc_type_game_mobile\">手机游戏</string>\n    <string name=\"ugc_type_game_mugen\">Mugen</string>\n    <string name=\"ugc_type_game_music\">音游</string>\n    <string name=\"ugc_type_game_online\">网络游戏</string>\n    <string name=\"ugc_type_game_stand_alone\">单机游戏</string>\n    <string name=\"ugc_type_information\">资讯</string>\n    <string name=\"ugc_type_information_global\">环球</string>\n    <string name=\"ugc_type_information_hotspot\">热点</string>\n    <string name=\"ugc_type_information_multiple\">综合</string>\n    <string name=\"ugc_type_information_social\">社会</string>\n    <string name=\"ugc_type_kichiku\">鬼畜</string>\n    <string name=\"ugc_type_kichiku_course\">教程演示</string>\n    <string name=\"ugc_type_kichiku_guild\">鬼畜调教</string>\n    <string name=\"ugc_type_kichiku_mad\">音MAD</string>\n    <string name=\"ugc_type_kichiku_manual_vocaloid\">人力VOCALOID</string>\n    <string name=\"ugc_type_kichiku_theatre\">鬼畜剧场</string>\n    <string name=\"ugc_type_knowledge\">知识</string>\n    <string name=\"ugc_type_knowledge_business\">财经商业</string>\n    <string name=\"ugc_type_knowledge_campus\">校园学习</string>\n    <string name=\"ugc_type_knowledge_career\">职业职场</string>\n    <string name=\"ugc_type_knowledge_design\">设计·创意</string>\n    <string name=\"ugc_type_knowledge_humanity\">人文历史</string>\n    <string name=\"ugc_type_knowledge_science\">科学科普</string>\n    <string name=\"ugc_type_knowledge_skill\">野生技术协会</string>\n    <string name=\"ugc_type_knowledge_social_science\">社科·法律·心理</string>\n    <string name=\"ugc_type_life\">生活</string>\n    <string name=\"ugc_type_life_daily\">日常</string>\n    <string name=\"ugc_type_life_funny\">搞笑</string>\n    <string name=\"ugc_type_life_hand_make\">手工</string>\n    <string name=\"ugc_type_life_home\">家居房产</string>\n    <string name=\"ugc_type_life_painting\">绘画</string>\n    <string name=\"ugc_type_life_parenting\">亲子</string>\n    <string name=\"ugc_type_life_rural_life\">三农</string>\n    <string name=\"ugc_type_life_travel\">出行</string>\n    <string name=\"ugc_type_music\">音乐</string>\n    <string name=\"ugc_type_music_ai_music\">AI音乐</string>\n    <string name=\"ugc_type_music_commentary\">乐评盘点</string>\n    <string name=\"ugc_type_music_cover\">翻唱</string>\n    <string name=\"ugc_type_music_fan_videos\">音乐粉丝饭拍</string>\n    <string name=\"ugc_type_music_live\">音乐现场</string>\n    <string name=\"ugc_type_music_mv\">MV</string>\n    <string name=\"ugc_type_music_original\">原创音乐</string>\n    <string name=\"ugc_type_music_other\">音乐综合</string>\n    <string name=\"ugc_type_music_perform\">演奏</string>\n    <string name=\"ugc_type_music_radio\">电台</string>\n    <string name=\"ugc_type_music_tutorial\">音乐教学</string>\n    <string name=\"ugc_type_music_vocaloid_utau\">VOCALOID·UTAU</string>\n    <string name=\"ugc_type_sports\">运动</string>\n    <string name=\"ugc_type_sports_aerobics\">健身</string>\n    <string name=\"ugc_type_sports_athletic\">竞技体育</string>\n    <string name=\"ugc_type_sports_basketball\">篮球</string>\n    <string name=\"ugc_type_sports_comprehensive\">运动综合</string>\n    <string name=\"ugc_type_sports_culture\">运动文化</string>\n    <string name=\"ugc_type_sports_football\">足球</string>\n    <string name=\"ugc_type_tech\">科技</string>\n    <string name=\"ugc_type_tech_application\">软件应用</string>\n    <string name=\"ugc_type_tech_computer_tech\">计算机技术</string>\n    <string name=\"ugc_type_tech_digital\">数码</string>\n    <string name=\"ugc_type_tech_diy\">极客DIY</string>\n    <string name=\"ugc_type_tech_industry\">科工机械</string>\n    <string name=\"ugc_type_v2_ai\">人工智能</string>\n    <string name=\"ugc_type_v2_ai_information\">AI资讯</string>\n    <string name=\"ugc_type_v2_ai_other\">AI杂谈</string>\n    <string name=\"ugc_type_v2_ai_tutorial\">AI学习</string>\n    <string name=\"ugc_type_v2_animal\">动物</string>\n    <string name=\"ugc_type_v2_animal_cat\">猫</string>\n    <string name=\"ugc_type_v2_animal_dog\">狗</string>\n    <string name=\"ugc_type_v2_animal_other\">动物综合·二创</string>\n    <string name=\"ugc_type_v2_animal_reptiles\">小宠异宠</string>\n    <string name=\"ugc_type_v2_animal_science\">野生动物·动物解说科普</string>\n    <string name=\"ugc_type_v2_car\">汽车</string>\n    <string name=\"ugc_type_v2_car_commentary\">汽车测评</string>\n    <string name=\"ugc_type_v2_car_culture\">汽车文化</string>\n    <string name=\"ugc_type_v2_car_life\">汽车生活</string>\n    <string name=\"ugc_type_v2_car_other\">汽车综合</string>\n    <string name=\"ugc_type_v2_car_tech\">汽车技术</string>\n    <string name=\"ugc_type_v2_cinephile\">影视</string>\n    <string name=\"ugc_type_v2_cinephile_ai\">AI影视</string>\n    <string name=\"ugc_type_v2_cinephile_commentary\">影视解读</string>\n    <string name=\"ugc_type_v2_cinephile_information\">影视资讯</string>\n    <string name=\"ugc_type_v2_cinephile_montage\">影视剪辑</string>\n    <string name=\"ugc_type_v2_cinephile_other\">影视综合</string>\n    <string name=\"ugc_type_v2_cinephile_porterage\">影视正片搬运</string>\n    <string name=\"ugc_type_v2_cinephile_reaction\">影视reaction</string>\n    <string name=\"ugc_type_v2_cinephile_shortfilm\">短剧短片</string>\n    <string name=\"ugc_type_v2_dance\">舞蹈</string>\n    <string name=\"ugc_type_v2_dance_ballet\">芭蕾舞</string>\n    <string name=\"ugc_type_v2_dance_china\">国风舞蹈</string>\n    <string name=\"ugc_type_v2_dance_gestures\">颜值·网红舞</string>\n    <string name=\"ugc_type_v2_dance_hiphop\">街舞</string>\n    <string name=\"ugc_type_v2_dance_otaku\">宅舞</string>\n    <string name=\"ugc_type_v2_dance_other\">舞蹈综合</string>\n    <string name=\"ugc_type_v2_dance_star\">明星舞蹈</string>\n    <string name=\"ugc_type_v2_dance_tutorial\">舞蹈教学</string>\n    <string name=\"ugc_type_v2_dance_wota\">wota艺</string>\n    <string name=\"ugc_type_v2_douga\">动画</string>\n    <string name=\"ugc_type_v2_douga_comic\">漫画·动态漫</string>\n    <string name=\"ugc_type_v2_douga_commentary\">动漫评论</string>\n    <string name=\"ugc_type_v2_douga_editing\">动漫剪辑</string>\n    <string name=\"ugc_type_v2_douga_fan_anime\">同人动画</string>\n    <string name=\"ugc_type_v2_douga_garage_kit\">模玩周边</string>\n    <string name=\"ugc_type_v2_douga_information\">动漫资讯</string>\n    <string name=\"ugc_type_v2_douga_interpret\">网文解读</string>\n    <string name=\"ugc_type_v2_douga_motion\">广播剧</string>\n    <string name=\"ugc_type_v2_douga_offline\">二次元线下</string>\n    <string name=\"ugc_type_v2_douga_osplay\">cosplay</string>\n    <string name=\"ugc_type_v2_douga_other\">二次元其他</string>\n    <string name=\"ugc_type_v2_douga_puppetry\">布袋戏</string>\n    <string name=\"ugc_type_v2_douga_quick_view\">动漫速读</string>\n    <string name=\"ugc_type_v2_douga_reaction\">动漫reaction</string>\n    <string name=\"ugc_type_v2_douga_tokusatsu\">特摄</string>\n    <string name=\"ugc_type_v2_douga_tutorial\">动漫教学</string>\n    <string name=\"ugc_type_v2_douga_voice\">动漫配音</string>\n    <string name=\"ugc_type_v2_douga_vup\">虚拟up主</string>\n    <string name=\"ugc_type_v2_emotion\">情感</string>\n    <string name=\"ugc_type_v2_emotion_family\">家庭关系</string>\n    <string name=\"ugc_type_v2_emotion_growth\">自我成长</string>\n    <string name=\"ugc_type_v2_emotion_interpersonal\">人际关系</string>\n    <string name=\"ugc_type_v2_emotion_romantic\">恋爱关系</string>\n    <string name=\"ugc_type_v2_ent\">娱乐</string>\n    <string name=\"ugc_type_v2_ent_commentary\">娱乐评论</string>\n    <string name=\"ugc_type_v2_ent_fans_video\">娱乐饭拍&amp;现场</string>\n    <string name=\"ugc_type_v2_ent_information\">娱乐资讯</string>\n    <string name=\"ugc_type_v2_ent_montage\">明星剪辑</string>\n    <string name=\"ugc_type_v2_ent_other\">娱乐综合</string>\n    <string name=\"ugc_type_v2_ent_reaction\">娱乐reaction</string>\n    <string name=\"ugc_type_v2_ent_variety\">娱乐综艺正片</string>\n    <string name=\"ugc_type_v2_fashion\">时尚美妆</string>\n    <string name=\"ugc_type_v2_fashion_accessories\">箱包配饰</string>\n    <string name=\"ugc_type_v2_fashion_commentary\">时尚解读</string>\n    <string name=\"ugc_type_v2_fashion_cos\">仿装cos</string>\n    <string name=\"ugc_type_v2_fashion_jewelry\">珠宝首饰</string>\n    <string name=\"ugc_type_v2_fashion_makeup\">美妆</string>\n    <string name=\"ugc_type_v2_fashion_other\">时尚综合</string>\n    <string name=\"ugc_type_v2_fashion_outfits\">鞋服穿搭</string>\n    <string name=\"ugc_type_v2_fashion_skincare\">护肤</string>\n    <string name=\"ugc_type_v2_fashion_trick\">三坑</string>\n    <string name=\"ugc_type_v2_food\">美食</string>\n    <string name=\"ugc_type_v2_food_commentary\">美食测评</string>\n    <string name=\"ugc_type_v2_food_detective\">美食探店</string>\n    <string name=\"ugc_type_v2_food_make\">美食制作</string>\n    <string name=\"ugc_type_v2_food_other\">美食综合</string>\n    <string name=\"ugc_type_v2_food_record\">美食记录</string>\n    <string name=\"ugc_type_v2_game\">游戏</string>\n    <string name=\"ugc_type_v2_game_act\">动作竞技游戏</string>\n    <string name=\"ugc_type_v2_game_mmorpg\">MMORPG游戏</string>\n    <string name=\"ugc_type_v2_game_moba\">MOBA游戏</string>\n    <string name=\"ugc_type_v2_game_msc\">音游舞游</string>\n    <string name=\"ugc_type_v2_game_other\">其他游戏</string>\n    <string name=\"ugc_type_v2_game_otome\">女性向游戏</string>\n    <string name=\"ugc_type_v2_game_puz\">休闲/小游戏</string>\n    <string name=\"ugc_type_v2_game_rpg\">单人RPG游戏</string>\n    <string name=\"ugc_type_v2_game_rts\">即时策略游戏</string>\n    <string name=\"ugc_type_v2_game_sandbox\">沙盒类</string>\n    <string name=\"ugc_type_v2_game_sim\">模拟经营游戏</string>\n    <string name=\"ugc_type_v2_game_slg\">SLG游戏</string>\n    <string name=\"ugc_type_v2_game_spg\">体育竞速游戏</string>\n    <string name=\"ugc_type_v2_game_stand_alone\">单机主机类游</string>\n    <string name=\"ugc_type_v2_game_stg\">射击游戏</string>\n    <string name=\"ugc_type_v2_game_tbs\">回合制策略游</string>\n    <string name=\"ugc_type_v2_gym\">健身</string>\n    <string name=\"ugc_type_v2_gym_figure\">健身身材展示</string>\n    <string name=\"ugc_type_v2_gym_other\">健身综合</string>\n    <string name=\"ugc_type_v2_gym_record\">健身记录</string>\n    <string name=\"ugc_type_v2_gym_science\">健身科普</string>\n    <string name=\"ugc_type_v2_gym_tutorial\">健身跟练教学</string>\n    <string name=\"ugc_type_v2_handmake\">手工</string>\n    <string name=\"ugc_type_v2_handmake_diy\">DIY玩具</string>\n    <string name=\"ugc_type_v2_handmake_handbook\">文具手帐</string>\n    <string name=\"ugc_type_v2_handmake_light\">轻手作</string>\n    <string name=\"ugc_type_v2_handmake_other\">其他手工</string>\n    <string name=\"ugc_type_v2_handmake_relief\">解压手工</string>\n    <string name=\"ugc_type_v2_handmake_traditional\">传统手工艺</string>\n    <string name=\"ugc_type_v2_health\">健康</string>\n    <string name=\"ugc_type_v2_health_asmr\">助眠视频·ASMR</string>\n    <string name=\"ugc_type_v2_health_other\">医疗保健综合</string>\n    <string name=\"ugc_type_v2_health_psychology\">心理健康</string>\n    <string name=\"ugc_type_v2_health_regimen\">养生</string>\n    <string name=\"ugc_type_v2_health_science\">健康科普</string>\n    <string name=\"ugc_type_v2_health_sexes\">两性知识</string>\n    <string name=\"ugc_type_v2_home\">家装房产</string>\n    <string name=\"ugc_type_v2_home_appliances\">家用电器</string>\n    <string name=\"ugc_type_v2_home_furniture\">家居展示</string>\n    <string name=\"ugc_type_v2_home_renovation\">家庭装修</string>\n    <string name=\"ugc_type_v2_home_trade\">买房租房</string>\n    <string name=\"ugc_type_v2_information\">资讯</string>\n    <string name=\"ugc_type_v2_information_other\">综合资讯</string>\n    <string name=\"ugc_type_v2_information_overseas\">海外资讯</string>\n    <string name=\"ugc_type_v2_information_politics\">时政资讯</string>\n    <string name=\"ugc_type_v2_information_social\">社会资讯</string>\n    <string name=\"ugc_type_v2_kichiku\">鬼畜</string>\n    <string name=\"ugc_type_v2_kichiku_guide\">鬼畜调教</string>\n    <string name=\"ugc_type_v2_kichiku_mad\">音MAD</string>\n    <string name=\"ugc_type_v2_kichiku_manual_vocaloid\">人力VOCALOID</string>\n    <string name=\"ugc_type_v2_kichiku_other\">鬼畜综合</string>\n    <string name=\"ugc_type_v2_kichiku_theatre\">鬼畜剧场</string>\n    <string name=\"ugc_type_v2_knowledge\">知识</string>\n    <string name=\"ugc_type_v2_knowledge_business\">商业财经</string>\n    <string name=\"ugc_type_v2_knowledge_campus\">大学专业知识</string>\n    <string name=\"ugc_type_v2_knowledge_career\">职场发展</string>\n    <string name=\"ugc_type_v2_knowledge_design\">设计艺术</string>\n    <string name=\"ugc_type_v2_knowledge_exam\">应试教育</string>\n    <string name=\"ugc_type_v2_knowledge_humanity_history\">人文历史</string>\n    <string name=\"ugc_type_v2_knowledge_lang_skill\">非应试语言学习</string>\n    <string name=\"ugc_type_v2_knowledge_other\">其他知识杂谈</string>\n    <string name=\"ugc_type_v2_knowledge_politics\">时政解读</string>\n    <string name=\"ugc_type_v2_knowledge_psychology\">心理杂谈</string>\n    <string name=\"ugc_type_v2_knowledge_science\">科学科普</string>\n    <string name=\"ugc_type_v2_knowledge_social_observation\">社会观察</string>\n    <string name=\"ugc_type_v2_life_experience\">生活经验</string>\n    <string name=\"ugc_type_v2_life_experience_marriage\">婚嫁</string>\n    <string name=\"ugc_type_v2_life_experience_procedures\">办事流程</string>\n    <string name=\"ugc_type_v2_life_experience_skills\">生活技能</string>\n    <string name=\"ugc_type_v2_life_joy\">生活兴趣</string>\n    <string name=\"ugc_type_v2_life_joy_artistic_products\">文玩文创</string>\n    <string name=\"ugc_type_v2_life_joy_leisure\">休闲玩乐</string>\n    <string name=\"ugc_type_v2_life_joy_on_site\">线下演出</string>\n    <string name=\"ugc_type_v2_life_joy_other\">兴趣综合</string>\n    <string name=\"ugc_type_v2_life_joy_trendy_toys\">潮玩玩具</string>\n    <string name=\"ugc_type_v2_music\">音乐</string>\n    <string name=\"ugc_type_v2_music_ai_music\">AI音乐</string>\n    <string name=\"ugc_type_v2_music_commentary\">乐评盘点</string>\n    <string name=\"ugc_type_v2_music_cover\">翻唱</string>\n    <string name=\"ugc_type_v2_music_fan_videos\">乐迷饭拍</string>\n    <string name=\"ugc_type_v2_music_live\">音乐现场</string>\n    <string name=\"ugc_type_v2_music_mv\">MV</string>\n    <string name=\"ugc_type_v2_music_original\">原创音乐</string>\n    <string name=\"ugc_type_v2_music_other\">音乐综合</string>\n    <string name=\"ugc_type_v2_music_perform\">演奏</string>\n    <string name=\"ugc_type_v2_music_radio\">电台·歌单</string>\n    <string name=\"ugc_type_v2_music_tutorial\">音乐教学</string>\n    <string name=\"ugc_type_v2_music_vocaloid\">VOCALOID</string>\n    <string name=\"ugc_type_v2_mysticism\">神秘学</string>\n    <string name=\"ugc_type_v2_mysticism_healing\">疗愈成长</string>\n    <string name=\"ugc_type_v2_mysticism_horoscope\">星座占星</string>\n    <string name=\"ugc_type_v2_mysticism_metaphysics\">传统玄学</string>\n    <string name=\"ugc_type_v2_mysticism_other\">其他神秘学</string>\n    <string name=\"ugc_type_v2_mysticism_tarot\">塔罗占卜</string>\n    <string name=\"ugc_type_v2_outdoors\">户外潮流</string>\n    <string name=\"ugc_type_v2_outdoors_camping\">露营</string>\n    <string name=\"ugc_type_v2_outdoors_explore\">户外探秘</string>\n    <string name=\"ugc_type_v2_outdoors_hiking\">徒步</string>\n    <string name=\"ugc_type_v2_outdoors_other\">户外综合</string>\n    <string name=\"ugc_type_v2_painting\">绘画</string>\n    <string name=\"ugc_type_v2_painting_acg\">二次元绘画</string>\n    <string name=\"ugc_type_v2_painting_none_acg\">非二次元绘画</string>\n    <string name=\"ugc_type_v2_painting_other\">绘画综合</string>\n    <string name=\"ugc_type_v2_painting_tutorial\">绘画学习</string>\n    <string name=\"ugc_type_v2_parenting\">亲子</string>\n    <string name=\"ugc_type_v2_parenting_cute\">萌娃</string>\n    <string name=\"ugc_type_v2_parenting_education\">亲子教育</string>\n    <string name=\"ugc_type_v2_parenting_infant_care\">婴幼护理</string>\n    <string name=\"ugc_type_v2_parenting_interaction\">亲子互动</string>\n    <string name=\"ugc_type_v2_parenting_other\">亲子综合</string>\n    <string name=\"ugc_type_v2_parenting_pregnant_care\">孕产护理</string>\n    <string name=\"ugc_type_v2_parenting_talent\">儿童才艺</string>\n    <string name=\"ugc_type_v2_rural\">三农</string>\n    <string name=\"ugc_type_v2_rural_fishing\">赶海捕鱼</string>\n    <string name=\"ugc_type_v2_rural_harvest\">打野采摘</string>\n    <string name=\"ugc_type_v2_rural_life\">农村生活</string>\n    <string name=\"ugc_type_v2_rural_planting\">农村种植</string>\n    <string name=\"ugc_type_v2_rural_tech\">农业技术</string>\n    <string name=\"ugc_type_v2_shortplay\">小剧场</string>\n    <string name=\"ugc_type_v2_shortplay_interview\">街头采访</string>\n    <string name=\"ugc_type_v2_shortplay_lang\">语言类小剧场</string>\n    <string name=\"ugc_type_v2_shortplay_plot\">剧情演绎</string>\n    <string name=\"ugc_type_v2_shortplay_up_variety\">UP主小综艺</string>\n    <string name=\"ugc_type_v2_sports\">体育运动</string>\n    <string name=\"ugc_type_v2_sports_badminton\">羽毛球</string>\n    <string name=\"ugc_type_v2_sports_basketball\">篮球</string>\n    <string name=\"ugc_type_v2_sports_fighting\">格斗</string>\n    <string name=\"ugc_type_v2_sports_football\">足球</string>\n    <string name=\"ugc_type_v2_sports_information\">体育资讯</string>\n    <string name=\"ugc_type_v2_sports_kungfu\">武术</string>\n    <string name=\"ugc_type_v2_sports_match\">体育赛事</string>\n    <string name=\"ugc_type_v2_sports_other\">体育综合</string>\n    <string name=\"ugc_type_v2_sports_running\">跑步</string>\n    <string name=\"ugc_type_v2_sports_trend\">潮流运动</string>\n    <string name=\"ugc_type_v2_tech\">科技数码</string>\n    <string name=\"ugc_type_v2_tech_computer\">电脑</string>\n    <string name=\"ugc_type_v2_tech_create\">自制发明/设备</string>\n    <string name=\"ugc_type_v2_tech_machine\">工程机械</string>\n    <string name=\"ugc_type_v2_tech_other\">科技数码综合</string>\n    <string name=\"ugc_type_v2_tech_pad\">平板电脑</string>\n    <string name=\"ugc_type_v2_tech_phone\">手机</string>\n    <string name=\"ugc_type_v2_tech_photography\">摄影摄像</string>\n    <string name=\"ugc_type_v2_travel\">旅游出行</string>\n    <string name=\"ugc_type_v2_travel_city\">城市出行</string>\n    <string name=\"ugc_type_v2_travel_record\">旅游记录</string>\n    <string name=\"ugc_type_v2_travel_strategy\">旅游攻略</string>\n    <string name=\"ugc_type_v2_travel_transport\">公共交通</string>\n    <string name=\"ugc_type_v2_vlog\">vlog</string>\n    <string name=\"ugc_type_v2_vlog_career\">职业vlog</string>\n    <string name=\"ugc_type_v2_vlog_life\">中外生活vlog</string>\n    <string name=\"ugc_type_v2_vlog_other\">其他vlog</string>\n    <string name=\"ugc_type_v2_vlog_student\">学生vlog</string>\n\n    <string name=\"user_homepage_anime\">我追的番</string>\n    <string name=\"user_homepage_favorite\">私人藏品</string>\n    <string name=\"user_homepage_follow\">已关注</string>\n    <string name=\"user_homepage_recent\">最近播放记录</string>\n    <string name=\"user_homepage_user_switch\">账户管理</string>\n    <string name=\"user_info_Incognito_mode_off\">未开启</string>\n    <string name=\"user_info_Incognito_mode_on\">已开启</string>\n    <string name=\"user_info_Incognito_mode_title\">隐身模式</string>\n    <string name=\"user_info_level\">Lv%1$s</string>\n    <string name=\"user_info_coins\">硬币: %1$s</string>\n    <string name=\"user_info_uid\">UID: %1$s</string>\n    <string name=\"user_lock_input_tip\">方向键输入密码 / Center 键确认输入 / 返回键退格</string>\n    <string name=\"user_lock_title_choose_user\">选择账户</string>\n    <string name=\"user_lock_title_input_new_password\">请输入新密码</string>\n    <string name=\"user_lock_title_input_new_password_again\">请再次输入新密码</string>\n    <string name=\"user_lock_title_input_old_password\">请输入旧密码</string>\n    <string name=\"user_lock_title_input_password\">输入密码</string>\n    <string name=\"user_lock_toast_password_different\">密码不一致</string>\n    <string name=\"user_lock_toast_password_error\">密码错误</string>\n    <string name=\"user_lock_toast_password_removed\">密码已移除</string>\n    <string name=\"user_switch_add_user\">添加账号</string>\n    <string name=\"user_switch_button_exit_manage_account\">退出管理</string>\n    <string name=\"user_switch_button_manage_account\">管理账号</string>\n    <string name=\"user_switch_menu_delete_account\">移除账号</string>\n    <string name=\"user_switch_menu_show_token\">显示用户凭证</string>\n    <string name=\"user_switch_menu_user_lock\">用户锁设置</string>\n    <string name=\"user_switch_title\">选择账号</string>\n\n    <string name=\"video_controller_menu_danmaku_area_tip\">按 “上键” 或 “下键” 进行调节</string>\n    <string name=\"video_info_argue_tip_vertical_screen\">温馨提示：该稿件包含竖屏视频内容</string>\n    <string name=\"video_info_description_title\">视频简介</string>\n    <string name=\"video_info_follow\">关注</string>\n    <string name=\"video_info_followed\">已关注</string>\n    <string name=\"video_info_part_row_title\">视频分 P</string>\n    <string name=\"video_info_related_video_title\">视频推荐</string>\n    <string name=\"video_info_tags\">标签</string>\n    <string name=\"video_info_time\">投稿时间: %1$s</string>\n    <string name=\"video_player_referer\">https://www.bilibili.com</string>\n\n    <string name=\"fans_count\">粉丝 %1$s</string>\n    <string name=\"friend_count\">关注 %1$s</string>\n    <string name=\"settings_portrait_video_fix_mode_title\">竖屏视频播放异常时的处理方式</string>\n    <string name=\"settings_portrait_video_fix_mode_text\">用来解决部分设备播放大于1080P的竖屏视频出现画面花屏/比例错误的问题</string>\n    <string name=\"settings_player_show_debug_info_text\">在视频播放页面右上角显示视频调试信息</string>\n    <string name=\"settings_player_show_debug_info_title\">显示视频调试信息</string>\n    <string name=\"settings_player_exit_when_all_is_played_text\">没有符合要求的下一个视频时，退出播放器回视频详情页</string>\n    <string name=\"settings_player_exit_when_all_is_played_title\">都播完后退出播放器</string>\n    <string name=\"settings_show_ugc_video_info_text\">关闭时，1、点击非PGC视频卡片不显示详情页直接开始播放，2、支持设置“UGC 视频播放页面的历史记录数量”</string>\n    <string name=\"settings_show_ugc_video_info_title\">显示UGC视频详情页</string>\n    <string name=\"settings_player_default_playback_speed_text\">设置视频播放的默认速度</string>\n    <string name=\"settings_player_default_playback_speed_title\">默认播放速度</string>\n    <string name=\"settings_player_seek_forward_step_text\">设置视频快进时间间隔（秒）</string>\n    <string name=\"settings_player_seek_forward_step_title\">视频快进时间间隔（秒）</string>\n    <string name=\"settings_player_seek_backward_step_text\">设置视频快退时间间隔（秒）</string>\n    <string name=\"settings_player_seek_backward_step_title\">视频快退时间间隔（秒）</string>\n    <string name=\"settings_player_show_bottom_progress_bar_text\">在播放器底部常驻显示迷你进度条</string>\n    <string name=\"settings_player_show_bottom_progress_bar_title\">显示迷你进度条</string>\n    <string name=\"settings_player_load_next_action_title\">自定义播放模式的规则</string>\n    <string name=\"settings_player_load_next_action_text\">设置“播放模式-自定义模式”查找下一个可播放视频的规则</string>\n    <string name=\"settings_player_load_next_action_do_nothing\">无，不播了</string>\n    <string name=\"settings_player_load_next_action_play_recommend\">推荐视频</string>\n    <string name=\"settings_player_load_next_action_play_next_part\">剧集和分P的下一个</string>\n    <string name=\"settings_player_load_next_action_play_next_part_or_recommend\">剧集和分P的下一个或推荐视频</string>\n    <string name=\"settings_player_default_start_position_title\">默认开始播放的位置</string>\n    <string name=\"settings_player_default_start_position_text\">设置视频有播放记录时开始播放的行为模式</string>\n    <string name=\"settings_player_default_start_position_history\">历史位置</string>\n    <string name=\"settings_player_default_start_position_beginning\">从开头开始</string>\n    <string name=\"player_skip_tip_back_to_history\">上次看到 %s 点击确认键跳转</string>\n    <string name=\"player_skip_tip_go_to_beginning\">点击确认键跳转到视频开头</string>\n    <string name=\"about_statement\">此项目是个人为了学习安卓开发而fork, 仅用于学习和测试，禁止在中国境内传播、宣传、分发，如有相关使用需求请使用 [哔哩哔哩官方APP](https://app.bilibili.com)，否则后果自负</string>\n</resources>\n"
  },
  {
    "path": "app/shared/src/main/res/values/themes.xml",
    "content": "<resources>\n\n    <style name=\"Theme.BV\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:navigationBarColor\">@android:color/transparent</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/shared/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config xmlns:tools=\"http://schemas.android.com/tools\">\n    <base-config cleartextTrafficPermitted=\"true\"\n        tools:ignore=\"InsecureBaseConfiguration\" />\n</network-security-config>"
  },
  {
    "path": "app/shared/src/main/res/xml/provider_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <cache-path name=\"cache\" path=\".\"/>\n</paths>"
  },
  {
    "path": "app/shared/src/r8Test/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <application>\n    </application>\n</manifest>"
  },
  {
    "path": "app/shared/src/r8Test/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<resources>\n    <string name=\"app_name\">BV R8 Test</string>\n</resources>"
  },
  {
    "path": "app/shared/src/test/kotlin/android/util/Log.kt",
    "content": "package android.util\n\nclass Log {\n    companion object {\n        @JvmStatic\n        fun isLoggable(tag: String, level: Int): Boolean {\n            return true\n        }\n\n        @JvmStatic\n        fun println(priority: Int, tag: String, msg: String): Int {\n            println(\"[$tag] $msg\")\n            return 0\n        }\n    }\n}"
  },
  {
    "path": "app/shared/src/test/kotlin/dev/aaa1115910/bv/network/GithubApiTest.kt",
    "content": "package dev.aaa1115910.bv.network\n\nimport kotlinx.coroutines.runBlocking\nimport kotlin.test.Test\n\nclass GithubApiTest {\n    @Test\n    fun `get latest release build`() = runBlocking {\n        println(GithubApi.getLatestReleaseBuild())\n    }\n\n    @Test\n    fun `get latest pre-release build`() = runBlocking {\n        println(GithubApi.getLatestPreReleaseBuild())\n    }\n}"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:ignore=\"MissingLeanbackSupport\">\n\n    <!-- 权限声明 -->\n    <uses-feature\n        android:name=\"android.hardware.touchscreen\"\n        android:required=\"false\" />\n\n    <uses-permission android:name=\"android.permission.READ_LOGS\" />\n\n    <application\n        tools:targetApi=\"tiramisu\"\n        android:hardwareAccelerated=\"true\"\n        android:banner=\"@drawable/ic_banner\"\n        android:icon=\"@drawable/ic_launcher\"\n        android:roundIcon=\"@drawable/ic_launcher_round\"\n        android:largeHeap=\"true\"\n        android:allowBackup=\"false\"\n        tools:replace=\"android:icon\"\n        tools:ignore=\"DataExtractionRules\">\n        <activity\n            android:name=\"dev.aaa1115910.bv.activities.LauncherActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/Theme.BV\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n                <category android:name=\"android.intent.category.LEANBACK_LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/tv/.gitignore",
    "content": "/build"
  },
  {
    "path": "app/tv/build.gradle.kts",
    "content": "plugins {\n    alias(gradleLibs.plugins.android.library)\n    alias(gradleLibs.plugins.compose.compiler)\n    alias(gradleLibs.plugins.google.ksp)\n    alias(gradleLibs.plugins.kotlin.android)\n    alias(gradleLibs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = AppConfiguration.appId+\".tv\"\n    compileSdk = AppConfiguration.compileSdk\n\n    defaultConfig {\n        minSdk = AppConfiguration.minSdk\n        vectorDrawables {\n            useSupportLibrary = true\n        }\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = true\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"r8Test\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"alpha\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n\n    buildFeatures {\n        compose = true\n    }\n\n    lint {\n        targetSdk = AppConfiguration.targetSdk\n    }\n\n    testOptions {\n        targetSdk = AppConfiguration.targetSdk\n    }\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\ndependencies {\n    implementation(project(\":app:shared\"))\n    implementation(libs.ui.util)\n}"
  },
  {
    "path": "app/tv/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "app/tv/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n\n# 设置优化轮数，可以提升代码优化效果\n-optimizationpasses 5\n# 允许改变访问修饰符，有助于优化\n-allowaccessmodification"
  },
  {
    "path": "app/tv/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:ignore=\"MissingLeanbackLauncher\">\n\n    <uses-feature\n        android:name=\"android.software.leanback\"\n        android:required=\"false\" />\n    <uses-feature\n        android:name=\"android.hardware.touchscreen\"\n        android:required=\"false\" />\n\n    <application tools:targetApi=\"tiramisu\">\n        <activity\n            android:name=\".activities.user.UserLockSettingsActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"true\"\n            android:label=\"@string/title_activity_user_lock_settings\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.video.RemoteControllerPanelDemoActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_remote_controller_panel_demo\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.settings.LogsActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_logs\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.user.UserSwitchActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"true\"\n            android:label=\"@string/title_activity_user_switch\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.settings.MediaCodecActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"true\"\n            android:label=\"@string/title_activity_media_codec\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.video.VideoPlayerV3Activity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"true\"\n            android:label=\"@string/title_activity_video_player_v3\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.video.TagActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_tag\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.pgc.PgcIndexActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_pgc_index\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.user.FollowingSeasonActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_following_season\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.pgc.anime.AnimeTimelineActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_anime_timeline\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.search.SearchResultActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_search_result\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.search.SearchInputActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_search_input\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.user.FollowActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_follow\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.video.SeasonInfoActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_season_info\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.settings.SpeedTestActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_speed_test\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.user.FavoriteActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_favorite\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.video.UpInfoActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_up_info\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.settings.SettingsActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_settings\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.user.HistoryActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_history\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.user.ToViewActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_toview\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.user.UserInfoActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_user_info\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.user.LoginActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_login\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\".activities.video.VideoInfoActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"false\"\n            android:label=\"@string/title_activity_video_info\"\n            android:theme=\"@style/Theme.BV.TV\" />\n        <activity\n            android:name=\"dev.aaa1115910.bv.tv.activities.MainActivity\"\n            android:banner=\"@drawable/ic_banner\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"true\"\n            android:screenOrientation=\"landscape\"\n            android:theme=\"@style/Theme.BV.TV.Splash\">\n        </activity>\n    </application>\n</manifest>"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/MainActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen\nimport dev.aaa1115910.bv.repository.UserRepository\nimport dev.aaa1115910.bv.tv.screens.MainScreen\nimport dev.aaa1115910.bv.tv.screens.RegionBlockScreen\nimport dev.aaa1115910.bv.tv.screens.user.lock.UnlockUserScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.NetworkUtil\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.android.ext.android.inject\n\nclass MainActivity : ComponentActivity() {\n\n    private val userRepository: UserRepository by inject()\n    private val logger = KotlinLogging.logger {}\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        var keepSplashScreen = true\n        installSplashScreen().apply {\n            setKeepOnScreenCondition { keepSplashScreen }\n        }\n        super.onCreate(savedInstanceState)\n\n        setContent {\n            val scope = rememberCoroutineScope()\n            var isCheckingUserLock by remember { mutableStateOf(true) }\n            var userLockLocked by remember { mutableStateOf(false) }\n\n            LaunchedEffect(Unit) {\n                scope.launch(Dispatchers.Default) {\n                    val user = userRepository.findUserByUid(userRepository.uid)\n                    userLockLocked = user?.lock?.isNotBlank() ?: false\n                    logger.info { \"default user: ${user?.username}\" }\n                    isCheckingUserLock = false\n                    keepSplashScreen = false\n                }\n            }\n\n            BVTheme {\n                if (isCheckingUserLock) {\n                    // 保持空白界面直到检查完成\n                } else {\n                    if (!userLockLocked) {\n                        MainScreen()\n                    } else {\n                        UnlockUserScreen(\n                            onUnlockSuccess = { user ->\n                                logger.info { \"unlock user lock for user ${user.uid}\" }\n                                userLockLocked = false\n                            }\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/pgc/PgcIndexActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.pgc\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.bv.tv.screens.main.pgc.PgcIndexScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass PgcIndexActivity : ComponentActivity() {\n    companion object {\n        fun actionStart(\n            context: Context,\n            pgcType: PgcType\n        ) {\n            context.startActivity(\n                Intent(context, PgcIndexActivity::class.java).apply {\n                    putExtra(\"pgcType\", pgcType.ordinal)\n                }\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                PgcIndexScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/pgc/anime/AnimeTimelineActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.pgc.anime\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.main.pgc.anime.AnimeTimelineScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass AnimeTimelineActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                AnimeTimelineScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/search/SearchInputActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.search\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.focus.FocusRequester\nimport dev.aaa1115910.bv.tv.screens.search.SearchInputScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass SearchInputActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            val defaultFocusRequester = remember { FocusRequester() }\n            BVTheme {\n                SearchInputScreen(defaultFocusRequester = defaultFocusRequester)\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/search/SearchResultActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.search\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.search.SearchResultScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass SearchResultActivity : ComponentActivity() {\n    companion object {\n        fun actionStart(context: Context, keyword: String, enableProxy: Boolean) {\n            context.startActivity(\n                Intent(context, SearchResultActivity::class.java).apply {\n                    putExtra(\"keyword\", keyword)\n                    putExtra(\"enableProxy\", enableProxy)\n                }\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                SearchResultScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/settings/LogsActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.settings\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.settings.LogsScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass LogsActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                LogsScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/settings/MediaCodecActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.settings\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.settings.MediaCodecScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass MediaCodecActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                MediaCodecScreen()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/settings/SettingsActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.settings\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.settings.SettingsScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass SettingsActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                SettingsScreen()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/settings/SpeedTestActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.settings\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.settings.SpeedTestScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass SpeedTestActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                SpeedTestScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/user/FavoriteActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.user\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.user.FavoriteScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass FavoriteActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                FavoriteScreen()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/user/FollowActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.user\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.user.FollowScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass FollowActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                FollowScreen()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/user/FollowingSeasonActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.user\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.user.FollowingSeasonScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass FollowingSeasonActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                FollowingSeasonScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/user/HistoryActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.user\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.user.HistoryScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass HistoryActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                HistoryScreen()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/user/LoginActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.user\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.login.LoginScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass LoginActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                LoginScreen()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/user/ToViewActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.user\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\n// import dev.aaa1115910.bv.screen.user.HistoryScreen\nimport dev.aaa1115910.bv.tv.screens.user.ToViewScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass ToViewActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                // HistoryScreen()\n                ToViewScreen()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/user/UserInfoActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.user\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.user.UserInfoScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass UserInfoActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                UserInfoScreen()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/user/UserLockSettingsActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.user\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.user.lock.UserLockSettingsScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass UserLockSettingsActivity : ComponentActivity() {\n\n    companion object {\n        fun actionStart(\n            context: Context,\n            uid: Long\n        ) {\n            context.startActivity(\n                Intent(context, UserLockSettingsActivity::class.java).apply {\n                    putExtra(\"uid\", uid)\n                }\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                UserLockSettingsScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/user/UserSwitchActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.user\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.user.UserSwitchScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass UserSwitchActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                UserSwitchScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/video/RemoteControllerPanelDemoActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.video\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport dev.aaa1115910.bv.tv.component.RemoteControlPanelDemo\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.Prefs\n\nclass RemoteControllerPanelDemoActivity : ComponentActivity() {\n    companion object {\n        fun actionStart(\n            context: Context,\n            avid: Long,\n            cid: Long,\n            title: String,\n            partTitle: String,\n            played: Int,\n            fromSeason: Boolean,\n            subType: Int? = null,\n            epid: Int? = null,\n            seasonId: Int? = null,\n            isVerticalVideo: Boolean = false,\n            proxyArea: ProxyArea = ProxyArea.MainLand,\n            playerIconIdle: String = \"\",\n            playerIconMoving: String = \"\",\n            play: Long = 0,\n            danmaku: Int = 0,\n            like: Int = 0,\n            coin: Int = 0,\n            favorite: Int = 0,\n            upName: String = \"\",\n            upId: Long = 0L,\n            upFace: String = \"\",\n            pubTime: String = \"\"\n        ) {\n            context.startActivity(\n                Intent(context, RemoteControllerPanelDemoActivity::class.java).apply {\n                    putExtra(\"avid\", avid)\n                    putExtra(\"cid\", cid)\n                    putExtra(\"title\", title)\n                    putExtra(\"partTitle\", partTitle)\n                    putExtra(\"played\", played)\n                    putExtra(\"fromSeason\", fromSeason)\n                    putExtra(\"subType\", subType)\n                    putExtra(\"epid\", epid)\n                    putExtra(\"seasonId\", seasonId)\n                    putExtra(\"isVerticalVideo\", isVerticalVideo)\n                    putExtra(\"proxy_area\", proxyArea.ordinal)\n                    putExtra(\"playerIconIdle\", playerIconIdle)\n                    putExtra(\"playerIconMoving\", playerIconMoving)\n                    putExtra(\"play\", play)\n                    putExtra(\"danmaku\", danmaku)\n                    putExtra(\"like\", like)\n                    putExtra(\"coin\", coin)\n                    putExtra(\"favorite\", favorite)\n                    putExtra(\"upName\", upName)\n                    putExtra(\"upId\", upId)\n                    putExtra(\"upFace\", upFace)\n                    putExtra(\"pubTime\", pubTime)\n                }\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                RemoteControllerPanelDemoScreen()\n            }\n        }\n    }\n}\n\n@Composable\nfun RemoteControllerPanelDemoScreen(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n    val intent = (context as Activity).intent\n\n    val continueToPlayerV3 = {\n        Prefs.showedRemoteControllerPanelDemo = true\n        dev.aaa1115910.bv.tv.activities.video.VideoPlayerV3Activity.actionStart(\n            context = context,\n            avid = intent.getLongExtra(\"avid\", 0),\n            cid = intent.getLongExtra(\"cid\", 0),\n            title = intent.getStringExtra(\"title\") ?: \"\",\n            partTitle = intent.getStringExtra(\"partTitle\") ?: \"\",\n            played = intent.getIntExtra(\"played\", 0),\n            fromSeason = intent.getBooleanExtra(\"fromSeason\", false),\n            subType = intent.getIntExtra(\"subType\", 0),\n            epid = intent.getIntExtra(\"epid\", 0),\n            seasonId = intent.getIntExtra(\"seasonId\", 0),\n            isVerticalVideo = intent.getBooleanExtra(\"isVerticalVideo\", false),\n            proxyArea = ProxyArea.entries[intent.getIntExtra(\"proxy_area\", 0)],\n            playerIconIdle = intent.getStringExtra(\"playerIconIdle\") ?: \"\",\n            playerIconMoving = intent.getStringExtra(\"playerIconMoving\") ?: \"\"\n        )\n        context.finish()\n    }\n\n    RemoteControlPanelDemo(\n        modifier = modifier.fillMaxSize(),\n        onConfirm = continueToPlayerV3\n    )\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/video/SeasonInfoActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.video\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.screens.SeasonInfoScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass SeasonInfoActivity : ComponentActivity() {\n    companion object {\n        fun actionStart(\n            context: Context,\n            epId: Int? = null,\n            seasonId: Int? = null,\n            proxyArea: ProxyArea = ProxyArea.MainLand\n        ) {\n            context.startActivity(\n                Intent(context, SeasonInfoActivity::class.java).apply {\n                    epId?.let { putExtra(\"epid\", epId) }\n                    seasonId?.let { putExtra(\"seasonid\", seasonId) }\n                    putExtra(\"proxy_area\", proxyArea.ordinal)\n                }\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                SeasonInfoScreen()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/video/TagActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.video\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.TagScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\nclass TagActivity : ComponentActivity() {\n    companion object {\n        fun actionStart(context: Context, tagId: Int, tagName: String) {\n            context.startActivity(\n                Intent(context, TagActivity::class.java).apply {\n                    putExtra(\"tagId\", tagId)\n                    putExtra(\"tagName\", tagName)\n                }\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContent {\n            BVTheme {\n                TagScreen()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/video/UpInfoActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.video\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.tv.screens.user.UpSpaceScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport java.lang.ref.WeakReference\nimport java.util.LinkedHashMap\n\nclass UpInfoActivity : ComponentActivity() {\n    companion object {\n        // 允许最多 N 个不同 mid 的页面共存\n        private const val MAX_SCREENS = 2\n    // LinkedHashMap 保持插入顺序：最早插入的条目位于 entrySet().iterator().next()\n    private val activityByMid = LinkedHashMap<Long, WeakReference<UpInfoActivity>>()\n\n        fun actionStart(context: Context, mid: Long, name: String, face: String) {\n            if (mid <= 0) return\n            context.startActivity(\n                Intent(context, UpInfoActivity::class.java).apply {\n                    putExtra(\"mid\", mid)\n                    putExtra(\"name\", name)\n                    putExtra(\"face\", face)\n                }\n            )\n        }\n    }\n\n    private var mid: Long = -1L\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        mid = intent.getLongExtra(\"mid\", -1L)\n        if (mid <= 0) {\n            finish()\n            return\n        }\n\n        synchronized(activityByMid) {\n            // 清理失效引用\n            val it = activityByMid.entries.iterator()\n            while (it.hasNext()) {\n                val entry = it.next()\n                val act = entry.value.get()\n                if (act == null || act.isFinishing) it.remove()\n            }\n            // 若已存在当前 mid，先关闭旧实例并移除，使重新插入刷新其“最近”位置\n            activityByMid[mid]?.get()?.let { old ->\n                if (old !== this && !old.isFinishing) old.finish()\n            }\n            activityByMid.remove(mid)\n            activityByMid[mid] = WeakReference(this)\n\n            // 超过最大不同 mid 数量：按插入顺序移除最早的非当前 mid\n            while (activityByMid.size > MAX_SCREENS) {\n                val iterator = activityByMid.entries.iterator()\n                var removed = false\n                while (iterator.hasNext()) {\n                    val oldest = iterator.next()\n                    if (oldest.key == mid) continue // 跳过当前，找真正最早的其它 mid\n                    oldest.value.get()?.let { act ->\n                        if (!act.isFinishing) act.finish()\n                    }\n                    iterator.remove()\n                    removed = true\n                    break\n                }\n                if (!removed) break // 只剩当前 mid\n            }\n        }\n\n        setContent {\n            BVTheme { UpSpaceScreen() }\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        synchronized(activityByMid) {\n            val ref = activityByMid[mid]\n            if (ref?.get() == this || ref?.get() == null) activityByMid.remove(mid)\n            val it = activityByMid.entries.iterator()\n            while (it.hasNext()) {\n                val entry = it.next()\n                val act = entry.value.get()\n                if (act == null || act.isFinishing) it.remove()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/video/VideoInfoActivity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.video\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.screens.VideoInfoScreen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.Prefs\nimport java.lang.ref.WeakReference\nimport java.util.LinkedList\n\nclass VideoInfoActivity : ComponentActivity() {\n    companion object {\n        // 使用WeakReference防止内存泄漏，避免持有已销毁Activity的强引用\n        private val activityQueue = LinkedList<WeakReference<VideoInfoActivity>>()\n\n        fun actionStart(\n            context: Context,\n            aid: Long,\n            cid: Long? = null,\n            fromSeason: Boolean = false,\n            fromPlayer: Boolean = false,\n            proxyArea: ProxyArea = ProxyArea.MainLand\n        ) {\n            context.startActivity(\n                Intent(context, VideoInfoActivity::class.java).apply {\n                    putExtra(\"aid\", aid)\n                    putExtra(\"cid\", cid)\n                    putExtra(\"fromSeason\", fromSeason)\n                    putExtra(\"fromPlayer\", fromPlayer)\n                    putExtra(\"proxy_area\", proxyArea.ordinal)\n                }\n            )\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        val fromPlayer = intent.getBooleanExtra(\"fromPlayer\", false)\n        val shouldRecordInHistoryQueue = !fromPlayer || Prefs.videoInfoHistoryIncludeFromPlayer\n\n        // 将当前活动加入队列\n        if (shouldRecordInHistoryQueue) {\n            synchronized(activityQueue) {\n                val maxVideoInfoScreens = Prefs.ugcVideoInfoHistoryCount.coerceAtLeast(1)\n\n                // 清理队列中的无效引用 - 这步是必要的\n                // 1. 确保队列大小计算准确，防止误判是否达到历史留存上限\n                // 2. 处理可能未正常触发onDestroy的情况（如系统回收、应用崩溃等）\n                // 3. 防止队列中累积无效引用导致内存泄漏\n                val iterator = activityQueue.iterator()\n                while (iterator.hasNext()) {\n                    val activityRef = iterator.next()\n                    val activity = activityRef.get()\n                    if (activity == null || activity.isFinishing) {\n                        iterator.remove()\n                    }\n                }\n\n                // 添加当前活动到队列\n                activityQueue.add(WeakReference(this))\n\n                // 如果队列超过了最大限制，关闭最早的活动\n                if (activityQueue.size > maxVideoInfoScreens) {\n                    // 移除最早的活动引用\n                    val oldestActivityRef = activityQueue.removeFirst()\n                    val oldestActivity = oldestActivityRef.get()\n                    // 确保在主线程调用finish()\n                    oldestActivity?.runOnUiThread {\n                        oldestActivity.finish()\n                    }\n                }\n            }\n        }\n\n        setContent {\n            BVTheme(\n                forceDark = true\n            ) {\n                VideoInfoScreen()\n            }\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n\n        // 当活动被销毁时，从队列中移除该Activity的引用\n        synchronized(activityQueue) {\n            val iterator = activityQueue.iterator()\n            while (iterator.hasNext()) {\n                val ref = iterator.next()\n                if (ref.get() == this || ref.get() == null) {\n                    iterator.remove()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/activities/video/VideoPlayerV3Activity.kt",
    "content": "package dev.aaa1115910.bv.tv.activities.video\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.WindowManager\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.lifecycle.lifecycleScope\nimport java.lang.ref.WeakReference\nimport java.util.LinkedList\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.PlayerType\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.player.VideoPlayerOptions\nimport dev.aaa1115910.bv.player.impl.exo.ExoPlayerFactory\nimport dev.aaa1115910.bv.tv.screens.VideoPlayerV3Screen\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.viewmodel.VideoPlayerV3ViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport org.koin.androidx.viewmodel.ext.android.viewModel\n\nclass VideoPlayerV3Activity : ComponentActivity() {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n        // 使用WeakReference防止内存泄漏，避免持有已销毁Activity的强引用\n        private val activityQueue = LinkedList<WeakReference<VideoPlayerV3Activity>>()\n\n        private fun formatPopularity(count: Int): String {\n            return when {\n                count >= 100_000_000 -> String.format(\"%.1f亿人气\", count / 100_000_000.0)\n                count >= 10_000 -> String.format(\"%.1f万人气\", count / 10_000.0)\n                else -> \"${count}人气\"\n            }\n        }\n        \n        /**\n         * 启动直播播放\n         */\n        fun actionStartLive(\n            context: Context,\n            roomId: Int,\n            title: String,\n            upName: String = \"\",\n            watchedNum: Int = 0,\n            upId: Long = 0L,\n            upFace: String = \"\"\n        ) {\n            val runtime = Runtime.getRuntime()\n            val usedMemory = runtime.totalMemory() - runtime.freeMemory()\n            val maxMemory = runtime.maxMemory()\n            logger.info { \"Current memory usage VideoPlayerV3Activity.actionStartLive: ${usedMemory / 1024 / 1024} MB / ${maxMemory / 1024 / 1024} MB\" }\n\n            context.startActivity(\n                Intent(\n                    context,\n                    VideoPlayerV3Activity::class.java\n                ).apply {\n                    putExtra(\"isLive\", true)\n                    putExtra(\"liveRoomId\", roomId)\n                    putExtra(\"title\", title)\n                    putExtra(\"upName\", upName)\n                    putExtra(\"liveWatchedNum\", watchedNum)\n                    putExtra(\"upId\", upId)\n                    putExtra(\"upFace\", upFace)\n                }\n            )\n        }\n        \n        fun actionStart(\n            context: Context,\n            avid: Long,\n            cid: Long,\n            title: String,\n            partTitle: String,\n            played: Int,\n            fromSeason: Boolean,\n            subType: Int? = null,\n            epid: Int? = null,\n            seasonId: Int? = null,\n            isVerticalVideo: Boolean = false,\n            proxyArea: ProxyArea = ProxyArea.MainLand,\n            playerIconIdle: String = \"\",\n            playerIconMoving: String = \"\",\n            play: Long = 0,\n            danmaku: Int = 0,\n            like: Int = 0,\n            coin: Int = 0,\n            favorite: Int = 0,\n            upName: String = \"\",\n            upId: Long = 0L,\n            upFace: String = \"\",\n            pubTime: String = \"\"\n        ) {\n            // 获取当前内存信息并打印到控制台\n            val runtime = Runtime.getRuntime()\n            val usedMemory = runtime.totalMemory() - runtime.freeMemory()\n            val maxMemory = runtime.maxMemory()\n            logger.info { \"Current memory usage VideoPlayerV3Activity.actionStart: ${usedMemory / 1024 / 1024} MB / ${maxMemory / 1024 / 1024} MB\" }\n\n            context.startActivity(\n                Intent(\n                    context,\n                    dev.aaa1115910.bv.tv.activities.video.VideoPlayerV3Activity::class.java\n                ).apply {\n                    putExtra(\"avid\", avid)\n                    putExtra(\"cid\", cid)\n                    putExtra(\"title\", title)\n                    putExtra(\"partTitle\", partTitle)\n                    putExtra(\"played\", played)\n                    putExtra(\"fromSeason\", fromSeason)\n                    putExtra(\"subType\", subType)\n                    putExtra(\"epid\", epid)\n                    putExtra(\"seasonId\", seasonId)\n                    putExtra(\"isVerticalVideo\", isVerticalVideo)\n                    putExtra(\"proxy_area\", proxyArea.ordinal)\n                    putExtra(\"playerIconIdle\", playerIconIdle)\n                    putExtra(\"playerIconMoving\", playerIconMoving)\n                    putExtra(\"play\", play)\n                    putExtra(\"danmaku\", danmaku)\n                    putExtra(\"like\", like)\n                    putExtra(\"coin\", coin)\n                    putExtra(\"favorite\", favorite)\n                    putExtra(\"upName\", upName)\n                    putExtra(\"upId\", upId)\n                    putExtra(\"upFace\", upFace)\n                    putExtra(\"pubTime\", pubTime)\n                }\n            )\n        }\n    }\n\n    private val playerViewModel: VideoPlayerV3ViewModel by viewModel()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        // 将当前活动加入队列\n        synchronized(activityQueue) {\n            val maxVideoPlayerScreens = if (Prefs.showUGCVideoInfo) {\n                1\n            } else {\n                Prefs.ugcVideoPlayerHistoryCount.coerceAtLeast(1)\n            }\n\n            // 清理队列中的无效引用\n            val iterator = activityQueue.iterator()\n            while (iterator.hasNext()) {\n                val activityRef = iterator.next()\n                val activity = activityRef.get()\n                if (activity == null || activity.isFinishing) {\n                    iterator.remove()\n                }\n            }\n\n            // 添加当前活动到队列\n            activityQueue.add(WeakReference(this))\n\n            // 如果队列超过了最大限制，关闭最早的活动\n            if (activityQueue.size > maxVideoPlayerScreens) {\n                val oldestActivityRef = activityQueue.removeFirst()\n                val oldestActivity = oldestActivityRef.get()\n                oldestActivity?.runOnUiThread {\n                    oldestActivity.finish()\n                }\n            }\n        }\n\n        initVideoPlayer()\n        //initDanmakuPlayer()\n        getParamsFromIntent()\n        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n        setContent {\n            BVTheme(\n                forceDark = true\n            ) {\n                VideoPlayerV3Screen()\n            }\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n\n        // 显式释放播放器资源，避免依赖 ViewModel.onCleared() 的延迟回调\n        if (isFinishing) {\n            playerViewModel.releasePlayerResources(\"onDestroy\")\n        }\n\n        // 当活动被销毁时，从队列中移除该Activity的引用\n        synchronized(activityQueue) {\n            val iterator = activityQueue.iterator()\n            while (iterator.hasNext()) {\n                val ref = iterator.next()\n                if (ref.get() == this || ref.get() == null) {\n                    iterator.remove()\n                }\n            }\n        }\n\n        // 获取当前内存信息并打印到控制台\n        val runtime = Runtime.getRuntime()\n        val usedMemory = runtime.totalMemory() - runtime.freeMemory()\n        val maxMemory = runtime.maxMemory()\n        logger.info { \"Current memory usage VideoPlayerV3Activity.onDestroy: ${usedMemory / 1024 / 1024} MB / ${maxMemory / 1024 / 1024} MB\" }\n    }\n\n    override fun onPause() {\n        playerViewModel.videoPlayer?.isInBackground = true\n        playerViewModel.videoPlayer?.pause()\n        \n        // 暂停直播弹幕\n        if (playerViewModel.isLive) {\n            playerViewModel.stopLiveDanmaku()\n        }\n\n        super.onPause()\n    }\n\n    override fun onResume() {\n        super.onResume()\n        // 直播从后台恢复时重新获取直播流，避免画面停留在之前的时间点\n        if (playerViewModel.isLive && playerViewModel.liveRoomId > 0) {\n            logger.info { \"Resume live stream for room ${playerViewModel.liveRoomId}\" }\n            playerViewModel.loadLiveStreamWithQuality(\n                playerViewModel.liveRoomId,\n                playerViewModel.currentLiveQn\n            )\n        }\n    }\n\n    private fun initVideoPlayer() {\n        dev.aaa1115910.bv.tv.activities.video.VideoPlayerV3Activity.Companion.logger.info { \"Init video player: ${Prefs.playerType.name}\" }\n        val options = VideoPlayerOptions(\n            userAgent = when (Prefs.apiType) {\n                ApiType.Web -> dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_WEB\n                ApiType.App -> dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_APP\n            },\n            referer = when (Prefs.apiType) {\n                ApiType.Web -> getString(R.string.video_player_referer)\n                ApiType.App -> null\n            },\n            enableFfmpegAudioRenderer = Prefs.enableFfmpegAudioRenderer,\n            enableAsyncQueueing = Prefs.enableAsyncQueueing\n        )\n        val videoPlayer = when (Prefs.playerType) {\n            PlayerType.Media3 -> ExoPlayerFactory().create(this, options)\n        }\n        playerViewModel.videoPlayer = videoPlayer\n    }\n\n    /*private fun initDanmakuPlayer() {\n        logger.info { \"Init danamku player\" }\n        runBlocking { playerViewModel.initDanmakuPlayer() }\n    }*/\n\n    private fun getParamsFromIntent() {\n        // 检查是否为直播模式\n        if (intent.getBooleanExtra(\"isLive\", false)) {\n            val roomId = intent.getIntExtra(\"liveRoomId\", 0)\n            val title = intent.getStringExtra(\"title\") ?: \"Unknown Title\"\n            val upName = intent.getStringExtra(\"upName\") ?: \"\"\n            val watchedNum = intent.getIntExtra(\"liveWatchedNum\", 0)\n            val upId = intent.getLongExtra(\"upId\", 0L)\n            val upFace = intent.getStringExtra(\"upFace\") ?: \"\"\n\n            logger.fInfo { \"Launch live parameter: [roomId=$roomId, watchedNum=$watchedNum]\" }\n            \n            playerViewModel.apply {\n                this.title = title\n                this.upName = upName\n                this.upId = upId\n                this.upFace = upFace\n                this.isLive = true\n                this.liveRoomId = roomId\n                this.livePopularityText = if (watchedNum > 0) formatPopularity(watchedNum) else \"\"\n                \n                // 通过 ViewModel 加载直播流（带画质选择，加载成功后自动启动弹幕）\n                loadLiveStreamWithQuality(roomId)\n            }\n            return\n        }\n        \n        if (intent.hasExtra(\"avid\")) {\n            val aid = intent.getLongExtra(\"avid\", 170001)\n            val cid = intent.getLongExtra(\"cid\", 170001)\n            val title = intent.getStringExtra(\"title\") ?: \"Unknown Title\"\n            val partTitle = intent.getStringExtra(\"partTitle\") ?: \"Unknown Part Title\"\n            val played = intent.getIntExtra(\"played\", 0)\n            val fromSeason = intent.getBooleanExtra(\"fromSeason\", false)\n            val subType = intent.getIntExtra(\"subType\", 0)\n            val epid = intent.getIntExtra(\"epid\", 0)\n            val seasonId = intent.getIntExtra(\"seasonId\", 0)\n            val isVerticalVideo = intent.getBooleanExtra(\"isVerticalVideo\", false)\n            val proxyArea = ProxyArea.entries[intent.getIntExtra(\"proxy_area\", 0)]\n            val playerIconIdle = intent.getStringExtra(\"playerIconIdle\") ?: \"\"\n            val playerIconMoving = intent.getStringExtra(\"playerIconMoving\") ?: \"\"\n            val play = intent.getLongExtra(\"play\", 0)\n            val danmaku = intent.getIntExtra(\"danmaku\", 0)\n            val like = intent.getIntExtra(\"like\", 0)\n            val coin = intent.getIntExtra(\"coin\", 0)\n            val favorite = intent.getIntExtra(\"favorite\", 0)\n            val upName = intent.getStringExtra(\"upName\") ?: \"\"\n            val upId = intent.getLongExtra(\"upId\", 0)\n            val upFace = intent.getStringExtra(\"upFace\") ?: \"\"\n            val pubTime = intent.getStringExtra(\"pubTime\") ?: \"\"\n            dev.aaa1115910.bv.tv.activities.video.VideoPlayerV3Activity.Companion.logger.fInfo { \"Launch parameter: [aid=$aid, cid=$cid]\" }\n            playerViewModel.apply {\n                // lastPlayed 需要在 loadPlayUrl 之前设置，以便 prepare() 时能正确设置初始跳转位置\n                this.lastPlayed = played\n                loadPlayUrl(\n                    avid = aid,\n                    cid = cid,\n                    epid = epid.takeIf { it != 0 }\n                )\n                this.title = title\n                this.partTitle = partTitle\n                this.fromSeason = fromSeason\n                this.subType = subType\n                this.epid = epid\n                this.seasonId = seasonId\n                this.isVerticalVideo = isVerticalVideo\n                this.proxyArea = proxyArea\n                this.playerIconIdle = playerIconIdle\n                this.playerIconMoving = playerIconMoving\n                this.play = play\n                this.danmaku = danmaku\n                this.like = like\n                this.coin = coin\n                this.favorite = favorite\n                this.upName = upName\n                this.upId = upId\n                this.upFace = upFace\n                this.pubTime = pubTime\n            }\n        } else {\n            dev.aaa1115910.bv.tv.activities.video.VideoPlayerV3Activity.Companion.logger.fInfo { \"Null launch parameter\" }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/Carousel.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedContentScope\nimport androidx.compose.animation.ContentTransform\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.CarouselDefaults\nimport androidx.tv.material3.ExperimentalTvMaterial3Api\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.CarouselData\nimport dev.aaa1115910.bv.util.focusedBorder\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\n@Composable\nfun PgcCarousel(\n    modifier: Modifier = Modifier,\n    data: List<CarouselData.CarouselItem>,\n    onClick: (CarouselData.CarouselItem) -> Unit\n) {\n    CarouselContent(\n        modifier = modifier,\n        data = data,\n        onClick = onClick\n    )\n}\n\n@Composable\nfun UgcCarousel(\n    modifier: Modifier = Modifier,\n    data: List<CarouselData.CarouselItem>,\n    onClick: (CarouselData.CarouselItem) -> Unit\n) {\n    CarouselContent(\n        modifier = modifier,\n        data = data,\n        onClick = onClick\n    )\n}\n\n@Composable\nfun CarouselContent(\n    modifier: Modifier = Modifier,\n    data: List<CarouselData.CarouselItem>,\n    onClick: (CarouselData.CarouselItem) -> Unit\n) {\n    Carousel(\n        itemCount = data.size,\n        modifier = modifier\n            .height(240.dp)\n            .clip(MaterialTheme.shapes.medium)\n            .focusedBorder(MaterialTheme.shapes.medium),\n        onClick = { itemIndex ->\n            onClick(data[itemIndex])\n        }\n    ) { itemIndex ->\n        CarouselCard(\n            data = data[itemIndex]\n        )\n    }\n}\n\n@Composable\nfun CarouselCard(\n    modifier: Modifier = Modifier,\n    data: CarouselData.CarouselItem\n) {\n    AsyncImage(\n        modifier = modifier.fillMaxWidth(),\n        model = data.cover,\n        contentDescription = null,\n        contentScale = ContentScale.Crop,\n        alignment = Alignment.TopCenter\n    )\n}\n\n@OptIn(ExperimentalTvMaterial3Api::class)\n@Composable\nfun Carousel(\n    itemCount: Int,\n    modifier: Modifier = Modifier,\n    autoScrollInterval: Long = CarouselDefaults.TimeToDisplayItemMillis,\n    contentTransformStartToEnd: ContentTransform = fadeIn(tween(1000))\n        .togetherWith(fadeOut(tween(1000))),\n    contentTransformEndToStart: ContentTransform = fadeIn(tween(1000))\n        .togetherWith(fadeOut(tween(1000))),\n    onClick: (index: Int) -> Unit,\n    content: @Composable AnimatedContentScope.(index: Int) -> Unit\n) {\n    var hasFocus by remember { mutableStateOf(false) }\n    var isMovingBackward by remember { mutableStateOf(false) }\n    var currentIndex by remember { mutableIntStateOf(0) }\n\n    LaunchedEffect(currentIndex, itemCount) {\n        while (true) {\n            delay(autoScrollInterval)\n            if (itemCount == 0 || hasFocus) continue\n            isMovingBackward = false\n            currentIndex = (currentIndex + 1) % itemCount\n        }\n    }\n\n    Box(\n        modifier = modifier\n            .onFocusChanged { focusState ->\n                hasFocus = focusState.isFocused\n            }\n            .clickable { onClick(currentIndex) }\n            .onKeyEvent {\n                when {\n                    itemCount == 0 -> false\n                    it.type == KeyEventType.KeyUp -> false\n                    it.key == Key.DirectionLeft -> {\n                        isMovingBackward = true\n                        currentIndex = (currentIndex - 1 + itemCount) % itemCount\n                        true\n                    }\n\n                    it.key == Key.DirectionRight -> {\n                        isMovingBackward = false\n                        currentIndex = (currentIndex + 1) % itemCount\n                        true\n                    }\n\n                    else -> false\n                }\n            }\n    ) {\n        AnimatedContent(\n            targetState = currentIndex,\n            transitionSpec = {\n                if (isMovingBackward) {\n                    contentTransformEndToStart\n                } else {\n                    contentTransformStartToEnd\n                }\n            },\n            label = \"CarouselAnimation\"\n        ) { activeItemIndex ->\n            if (itemCount > 0) content(activeItemIndex)\n        }\n        CarouselDefaults.IndicatorRow(\n            itemCount = itemCount,\n            activeItemIndex = currentIndex,\n            modifier = Modifier\n                .align(Alignment.BottomEnd)\n                .padding(16.dp),\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun CarouselPreview() {\n    val colors = remember { mutableStateListOf<Color>() }\n\n    val scope = rememberCoroutineScope()\n    LaunchedEffect(Unit) {\n        scope.launch(Dispatchers.IO) {\n            delay(8000)\n            colors.addAll(\n                listOf(\n                    Color.Red,\n                    Color.Yellow,\n                    Color.Green,\n                    Color.Blue,\n                    Color.Cyan,\n                    Color.Magenta,\n                    Color.Gray,\n                )\n            )\n        }\n    }\n\n    Column {\n        Button(onClick = {}) { Text(text = \"button\") }\n        Row {\n            Button(onClick = {}) { Text(text = \"button\") }\n            Carousel(\n                itemCount = colors.size,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(200.dp)\n                    .clip(MaterialTheme.shapes.medium)\n                    .focusedBorder(MaterialTheme.shapes.medium),\n                onClick = {\n\n                }\n            ) {\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .fillMaxHeight()\n                        .background(color = colors[it])\n                ) {}\n            }\n        }\n\n        Button(onClick = {}) { Text(text = \"button\") }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/CommentItem.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.foundation.text.appendInlineContent\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.rounded.ThumbUp\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.Placeholder\nimport androidx.compose.ui.text.PlaceholderVerticalAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport androidx.compose.ui.layout.ContentScale\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.biliapi.entity.reply.EmoteSize\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.focusedBorder\n\n/**\n * 评论列表项组件\n *\n * @param comment 评论数据\n * @param modifier 修饰符\n * @param onClick 点击回调\n * @param onLongClick 长按回调\n */\n@Composable\nfun CommentItem(\n    comment: Comment,\n    modifier: Modifier = Modifier,\n    onClick: () -> Unit = {},\n    onLongClick: () -> Unit = {}\n) {\n    Surface(\n        modifier = modifier\n            .fillMaxWidth()\n            .focusedBorder(MaterialTheme.shapes.small),\n        onClick = onClick,\n        onLongClick = onLongClick,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = Color.Transparent,\n            focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),\n            pressedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)\n        ),\n        scale = ClickableSurfaceDefaults.scale(\n            focusedScale = 1f,\n            pressedScale = 1f\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.small)\n    ) {\n        Column(\n            modifier = Modifier.padding(12.dp),\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            // 主评论\n            CommentMainContent(comment = comment)\n        }\n    }\n}\n\n/**\n * 主评论内容\n */\n@Composable\nprivate fun CommentMainContent(\n    comment: Comment\n) {\n    Row(\n        horizontalArrangement = Arrangement.spacedBy(12.dp),\n        verticalAlignment = Alignment.Top\n    ) {\n        // 用户头像\n        AsyncImage(\n            modifier = Modifier\n                .size(40.dp)\n                .clip(CircleShape)\n                .background(MaterialTheme.colorScheme.surface),\n            model = comment.member.avatar,\n            contentDescription = null,\n        )\n\n        // 评论内容\n        Column(\n            modifier = Modifier.weight(1f),\n            verticalArrangement = Arrangement.spacedBy(4.dp)\n        ) {\n            // 用户名\n            Text(\n                text = comment.member.name,\n                style = MaterialTheme.typography.titleSmall,\n                color = Color.White,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis\n            )\n\n            // 评论内容（支持表情）\n            CommentContent(\n                content = comment.content,\n                emotes = comment.emotes,\n                modifier = Modifier.padding(top = 4.dp)\n            )\n\n            // 评论图片\n            if (comment.pictures.isNotEmpty()) {\n                CommentPictures(\n                    pictures = comment.pictures,\n                    modifier = Modifier.padding(top = 4.dp)\n                )\n            }\n\n            // 底部信息：时间和点赞数\n            Row(\n                modifier = Modifier.padding(top = 4.dp),\n                horizontalArrangement = Arrangement.spacedBy(16.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                // 时间\n                Text(\n                    text = comment.timeDesc,\n                    style = MaterialTheme.typography.bodySmall,\n                    color = Color.White.copy(alpha = 0.5f)\n                )\n\n                // 点赞数\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(4.dp),\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Icon(\n                        modifier = Modifier.size(14.dp),\n                        imageVector = androidx.compose.material.icons.Icons.Rounded.ThumbUp,\n                        contentDescription = null,\n                        tint = Color.White.copy(alpha = 0.5f)\n                    )\n                    Text(\n                        text = formatLikeCount(comment.like),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White.copy(alpha = 0.5f)\n                    )\n                }\n\n                // 回复数\n                if (comment.repliesCount > 0) {\n                    Text(\n                        text = \"${comment.repliesCount} 回复\",\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White.copy(alpha = 0.5f)\n                    )\n                }\n            }\n        }\n    }\n}\n\n/**\n * 评论内容组件，支持表情显示（富文本）\n */\n@Composable\nfun CommentContent(\n    content: List<String>,\n    emotes: List<Comment.Emote>,\n    modifier: Modifier = Modifier,\n    maxLines: Int = Int.MAX_VALUE,\n    overflow: TextOverflow = TextOverflow.Clip\n) {\n    val emoteNameList = emotes.map { it.text }\n    val inlineContentMap = emotes.associateWith { emote ->\n        InlineTextContent(\n            Placeholder(\n                width = emote.size.fontSize.sp,\n                height = emote.size.fontSize.sp,\n                placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter\n            )\n        ) {\n            AsyncImage(\n                model = emote.url,\n                contentDescription = null\n            )\n        }\n    }.mapKeys { it.key.text }\n\n    Text(\n        modifier = modifier,\n        text = buildAnnotatedString {\n            content.forEach { text ->\n                if (emoteNameList.contains(text)) {\n                    appendInlineContent(text)\n                } else {\n                    append(text)\n                }\n            }\n        },\n        inlineContent = inlineContentMap,\n        style = MaterialTheme.typography.bodyMedium,\n        color = Color.White,\n        maxLines = maxLines,\n        overflow = overflow\n    )\n}\n\n/**\n * 格式化点赞数\n */\nprivate fun formatLikeCount(count: Long): String {\n    return when {\n        count >= 10000 -> \"${count / 10000}万\"\n        else -> count.toString()\n    }\n}\n\n/**\n * 生成 B 站图片缩略图 URL\n *\n * 参数格式：@{w}w_{h}h_{flags}.webp\n * dpr 固定为 2，格式固定 webp\n */\nprivate fun buildThumbnailUrl(\n    url: String,\n    w: Int = 0,\n    h: Int = 0,\n    crop: Boolean = false,\n    progressive: Boolean = true,\n    dpr: Int = 2\n): String {\n    val baseUrl = url.split(\"@\")[0].replace(\"//pre-\", \"//\")\n    val parts = mutableListOf<String>()\n    if (w > 0) parts.add(\"${w * dpr}w\")\n    if (h > 0) parts.add(\"${h * dpr}h\")\n    if (crop) parts.add(\"1c\")\n    if (progressive) parts.add(\"1s\")\n    return if (parts.isNotEmpty()) \"$baseUrl@${parts.joinToString(\"_\")}.webp\" else baseUrl\n}\n\nprivate data class CommentPictureItem(\n    val width: Int,\n    val height: Int,\n    val thumbnailUrl: String,\n    val original: Picture\n)\n\n/**\n * 计算评论图片的展示尺寸和缩略图 URL\n *\n * 与 bilibili PC 评论区 bili-comment-pictures-renderer 逻辑一致：\n * - 单图：横图框 240×135，竖图框 135×180，超长图裁剪\n * - 多图：统一 88×88 裁剪\n */\nprivate fun calculatePictureItems(pictures: List<Picture>): List<CommentPictureItem> {\n    val isSingle = pictures.size == 1\n    val multipleSize = 88\n\n    return pictures.map { pic ->\n        val imgW = pic.width\n        val imgH = pic.height\n        val isHorizontal = imgW > imgH\n        val ratio = if (isHorizontal) imgW.toFloat() / imgH else imgH.toFloat() / imgW\n        val isLong = kotlin.math.floor(ratio.toDouble()).toInt() >= 3\n\n        if (isSingle) {\n            val singleHorizontal = 240 to 135\n            val singleVertical = 135 to 180\n            val targetRatio = imgW.toFloat() / imgH\n            var w: Int\n            var h: Int\n\n            if (isLong) {\n                val frame = if (isHorizontal) singleHorizontal else singleVertical\n                w = frame.first\n                h = frame.second\n            } else if (!isHorizontal && imgW > singleVertical.first && imgH > singleVertical.second) {\n                w = singleVertical.first\n                h = singleVertical.second\n            } else if (isHorizontal) {\n                val frameRatio = singleHorizontal.first.toFloat() / singleHorizontal.second\n                if (targetRatio > frameRatio) {\n                    w = singleHorizontal.first\n                    h = (w / targetRatio).toInt()\n                } else {\n                    h = singleHorizontal.second\n                    w = (h * targetRatio).toInt()\n                }\n                if (w > imgW) { w = imgW; h = imgH }\n            } else {\n                val frameRatio = singleVertical.first.toFloat() / singleVertical.second\n                if (targetRatio > frameRatio) {\n                    w = singleVertical.first\n                    h = (w / targetRatio).toInt()\n                } else {\n                    h = singleVertical.second\n                    w = (h * targetRatio).toInt()\n                }\n                if (w > imgW) { w = imgW; h = imgH }\n            }\n\n            val thumbUrl = buildThumbnailUrl(\n                pic.url, w = w, h = h,\n                crop = isLong, progressive = true\n            )\n            CommentPictureItem(width = w, height = h, thumbnailUrl = thumbUrl, original = pic)\n        } else {\n            val thumbUrl = buildThumbnailUrl(\n                pic.url, w = multipleSize, h = multipleSize,\n                crop = true, progressive = true\n            )\n            CommentPictureItem(\n                width = multipleSize, height = multipleSize,\n                thumbnailUrl = thumbUrl, original = pic\n            )\n        }\n    }\n}\n\n/**\n * 评论图片组件\n *\n * - 单图：保持比例，宽度不超过内容区\n * - 多图：一行最多 2 张，正方形裁剪\n */\n@Composable\nfun CommentPictures(\n    pictures: List<Picture>,\n    modifier: Modifier = Modifier\n) {\n    val validPictures = remember(pictures) {\n        pictures.filter { it.url.isNotBlank() && it.width > 0 && it.height > 0 }\n    }\n    if (validPictures.isEmpty()) return\n\n    val items = remember(validPictures) { calculatePictureItems(validPictures) }\n    val isSingle = validPictures.size == 1\n\n    if (isSingle) {\n        val item = items.first()\n        val ratio = if (item.height > 0) item.width.toFloat() / item.height else 1f\n        AsyncImage(\n            model = item.thumbnailUrl,\n            contentDescription = null,\n            modifier = modifier\n                .widthIn(max = item.width.dp)\n                .aspectRatio(ratio)\n                .clip(RoundedCornerShape(6.dp)),\n            contentScale = ContentScale.Crop\n        )\n    } else {\n        val rows = items.chunked(2)\n        Column(\n            modifier = modifier,\n            verticalArrangement = Arrangement.spacedBy(6.dp)\n        ) {\n            rows.forEach { rowItems ->\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(6.dp)\n                ) {\n                    rowItems.forEach { item ->\n                        AsyncImage(\n                            model = item.thumbnailUrl,\n                            contentDescription = null,\n                            modifier = Modifier\n                                .size(item.width.dp)\n                                .clip(RoundedCornerShape(6.dp)),\n                            contentScale = ContentScale.Crop\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Preview(uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun CommentItemPreview() {\n    BVTheme {\n        CommentItem(\n            comment = Comment(\n                rpid = 123456,\n                mid = 789,\n                oid = 12345,\n                type = 1,\n                parent = 0,\n                content = listOf(\"这是一条测试评论\", \"[2333]\", \"后面还有内容\"),\n                member = Comment.Member(\n                    mid = 789,\n                    avatar = \"\",\n                    name = \"测试用户\"\n                ),\n                timeDesc = \"2小时前\",\n                emotes = listOf(\n                    Comment.Emote(\n                        text = \"[2333]\",\n                        url = \"https://i0.hdslb.com/bfs/emote/4352e2396c13e4150786d48e464d517174845b9c.png\",\n                        size = EmoteSize.Small\n                    )\n                ),\n                pictures = emptyList(),\n                replies = emptyList(),\n                repliesCount = 5,\n                like = 12345L\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/CommentPanel.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.gestures.animateScrollBy\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Border\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.SurfaceDefaults\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.biliapi.entity.reply.CommentPage\nimport dev.aaa1115910.biliapi.entity.reply.CommentSort\nimport dev.aaa1115910.biliapi.entity.video.season.Episode\nimport dev.aaa1115910.biliapi.entity.video.season.Section\nimport dev.aaa1115910.biliapi.repositories.CommentRepository\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.isDpadDown\nimport dev.aaa1115910.bv.util.isDpadLeft\nimport dev.aaa1115910.bv.util.isKeyDown\nimport dev.aaa1115910.bv.util.onBackPressed\nimport dev.aaa1115910.bv.util.requestFocus\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.koin.compose.getKoin\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.resizedImageUrl\n\n/**\n * 评论浮层组件\n *\n * @param show 是否显示浮层\n * @param oid 视频 aid\n * @param onHide 关闭浮层回调\n * @param episodes 正片剧集列表（用于选集切换）\n * @param sections 章节选集列表（用于选集切换）\n * @param initialEpisodeId 初始选中的剧集ID\n * @param onEpisodeChange 剧集切换回调\n */\n@Composable\nfun CommentPanel(\n    show: Boolean,\n    oid: Long,\n    onHide: () -> Unit,\n    episodes: List<Episode> = emptyList(),\n    sections: List<Section> = emptyList(),\n    initialEpisodeId: Int = -1,\n    onEpisodeChange: ((Episode) -> Unit)? = null\n) {\n    val commentRepository: CommentRepository = getKoin().get()\n    val scope = rememberCoroutineScope()\n    val listState = rememberLazyListState()\n    val focusRequester = remember { FocusRequester() }\n    val sidebarFocusRequester = remember { FocusRequester() }\n    val density = LocalDensity.current\n\n    val comments = remember { mutableStateListOf<Comment>() }\n    var loading by remember { mutableStateOf(false) }\n    var currentPage by remember { mutableStateOf(CommentPage()) }\n    var hasNext by remember { mutableStateOf(true) }\n    var error by remember { mutableStateOf<String?>(null) }\n\n    // 子评论浮窗状态\n    var showSubCommentPanel by remember { mutableStateOf(false) }\n    var selectedRootComment by remember { mutableStateOf<Comment?>(null) }\n    var hasRequestedFocus by remember { mutableStateOf(false) }\n    var wasSubCommentPanelShown by remember { mutableStateOf(false) }\n    var selectedCommentIndex by remember { mutableStateOf(0) }\n    var focusedCommentIndex by remember { mutableStateOf(0) }\n\n    // 全屏图片查看器状态\n    var showImageViewer by remember { mutableStateOf(false) }\n    var imageViewerPictures by remember { mutableStateOf<List<dev.aaa1115910.biliapi.entity.Picture>>(emptyList()) }\n\n    // 选集相关状态\n    var currentEpisode by remember { mutableStateOf<Episode?>(null) }\n    var focusOnSidebar by remember { mutableStateOf(false) }\n    var sidebarFocusRequestToken by remember { mutableIntStateOf(0) }\n    var pendingFocusToComments by remember { mutableStateOf(false) }\n\n    // 合并所有剧集（正片 + 章节）\n    val allEpisodeItems by remember(episodes, sections) {\n        derivedStateOf {\n            buildList {\n                var idx = 0\n                // 添加正片剧集\n                if (episodes.isNotEmpty()) {\n                    episodes.forEach { ep ->\n                        // 过滤掉 aid 为 0 的剧集\n                        if (ep.aid > 0) {\n                            add(EpisodeItem(ep, \"正片\", idx++))\n                        }\n                    }\n                }\n                // 添加章节剧集\n                sections.forEach { section ->\n                    section.episodes.forEach { ep ->\n                        // 过滤掉 aid 为 0 的剧集\n                        if (ep.aid > 0) {\n                            add(EpisodeItem(ep, section.title, idx++))\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // 是否显示侧边栏（多于1集时显示）\n    val showSidebar by remember(allEpisodeItems) {\n        derivedStateOf { allEpisodeItems.size > 1 }\n    }\n\n    fun requestFocusToCurrentEpisode() {\n        if (!showSidebar) return\n        focusOnSidebar = true\n        sidebarFocusRequestToken++\n    }\n\n    // 初始化当前选中的剧集\n    LaunchedEffect(allEpisodeItems, initialEpisodeId) {\n        if (currentEpisode == null && allEpisodeItems.isNotEmpty()) {\n            currentEpisode = if (initialEpisodeId != -1) {\n                allEpisodeItems.find { it.episode.id == initialEpisodeId }?.episode\n            } else {\n                allEpisodeItems.firstOrNull()?.episode\n            }\n        }\n    }\n\n    // 获取当前要加载评论的 aid\n    val currentOid by remember(currentEpisode, oid) {\n        derivedStateOf { currentEpisode?.aid ?: oid }\n    }\n\n    // 判断是否滚动到底部\n    val isAtBottom by remember {\n        derivedStateOf {\n            listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == comments.size - 1\n        }\n    }\n\n    // 加载评论\n    val loadComments: (reset: Boolean) -> Unit = { reset ->\n        scope.launch {\n            if (loading) return@launch\n            loading = true\n            error = null\n\n            try {\n                val page = if (reset) CommentPage() else currentPage\n                val data = commentRepository.getComments(\n                    id = currentOid,\n                    type = 1L, // 视频评论\n                    sort = CommentSort.Hot,\n                    page = page,\n                    preferApiType = Prefs.apiType\n                )\n\n                if (reset) {\n                    comments.clear()\n                    comments.addAll(data.comments)\n                } else {\n                    comments.addAll(data.comments)\n                }\n\n                currentPage = data.nextPage\n                hasNext = data.hasNext\n            } catch (e: Exception) {\n                error = e.message ?: \"加载失败\"\n            } finally {\n                loading = false\n            }\n        }\n    }\n\n    // 显示时加载评论\n    LaunchedEffect(show, currentOid) {\n        if (show && comments.isEmpty()) {\n            loadComments(true)\n        }\n        if (!show) {\n            hasRequestedFocus = false\n            wasSubCommentPanelShown = false\n            // 重置边栏焦点状态，确保下次打开时焦点在评论列表\n            focusOnSidebar = false\n            sidebarFocusRequestToken = 0\n            pendingFocusToComments = false\n        }\n    }\n\n    // 显示后请求焦点（初次显示时或子评论浮窗关闭后）\n    LaunchedEffect(show, showSubCommentPanel, comments.isNotEmpty(), loading) {\n        if (show && !showSubCommentPanel) {\n            // 子评论浮窗刚关闭，需要恢复焦点到之前点击的评论\n            if (wasSubCommentPanelShown) {\n                delay(300) // 等待动画完成\n                listState.scrollToItem(selectedCommentIndex)\n                delay(100)\n                focusRequester.requestFocus(scope)\n                wasSubCommentPanelShown = false\n            }\n            // 切换剧集后评论加载完成，请求焦点到评论列表\n            else if (pendingFocusToComments) {\n                delay(100) // 等待渲染完成\n                if (!loading) {\n                    delay(100) // 等待渲染完成\n                    if (comments.isNotEmpty() || !showSidebar || error != null) {\n                        focusRequester.requestFocus(scope)\n                    } else {\n                        requestFocusToCurrentEpisode()\n                    }\n                    pendingFocusToComments = false\n                }\n            }\n            // 初次显示父评论浮窗，请求焦点\n            else if (!hasRequestedFocus) {\n                delay(200) // 等待请求完成\n                if(!loading){\n                    delay(200) // 等待动画完成\n                    if (comments.isNotEmpty() || !showSidebar || error != null) {\n                        focusRequester.requestFocus(scope)\n                    } else {\n                        requestFocusToCurrentEpisode()\n                    }\n                    hasRequestedFocus = true\n                }\n            }\n        }\n        // 记录子评论浮窗显示状态\n        if (showSubCommentPanel) {\n            wasSubCommentPanelShown = true\n        }\n    }\n\n    // 懒加载：滚动到底部时加载更多\n    LaunchedEffect(isAtBottom, hasNext, loading) {\n        if (isAtBottom && hasNext && !loading && comments.isNotEmpty()) {\n            loadComments(false)\n        }\n    }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .clickable(onClick = onHide),\n        contentAlignment = Alignment.CenterEnd\n    ) {\n        AnimatedVisibility(\n            visible = show && !showSubCommentPanel,\n            enter = expandHorizontally(expandFrom = Alignment.End),\n            exit = shrinkHorizontally(shrinkTowards = Alignment.End)\n        ) {\n            Surface(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .padding(horizontal = 16.dp, vertical = 16.dp)\n                    .widthIn(\n                        min = if (showSidebar) 620.dp else 320.dp,\n                        max = if (showSidebar) 720.dp else 420.dp\n                    )\n                    .fillMaxWidth(if (showSidebar) 0.5f else 0.3f)\n                    .clickable(enabled = true, onClick = {}) // 阻止点击穿透\n                    .onBackPressed {\n                        if (focusOnSidebar && showSidebar) {\n                            // 在边栏时按返回键关闭评论面板\n                            onHide()\n                        } else if (showSidebar) {\n                            // 在评论列表时按返回键切换到边栏\n                            requestFocusToCurrentEpisode()\n                        } else {\n                            // 没有边栏时直接关闭\n                            onHide()\n                        }\n                    },\n                colors = SurfaceDefaults.colors(\n                    containerColor = Color.Black.copy(alpha = 0.85f)\n                ),\n                shape = MaterialTheme.shapes.large\n            ) {\n                Row(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .padding(16.dp),\n                    horizontalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    // 左侧边栏 - 仅在有多个剧集时显示\n                    if (showSidebar) {\n                        EpisodeSidebar(\n                            episodes = allEpisodeItems,\n                            currentEpisode = currentEpisode,\n                            onEpisodeSelected = { episodeItem ->\n                                currentEpisode = episodeItem.episode\n                                onEpisodeChange?.invoke(episodeItem.episode)\n                                comments.clear()\n                                loadComments(true)\n                                // 切换剧集后将焦点移回评论列表\n                                focusOnSidebar = false\n                                // 标记需要在评论加载完成后请求焦点\n                                pendingFocusToComments = true\n                            },\n                            modifier = Modifier\n                                .width(220.dp)\n                                .fillMaxHeight(),\n                            focusRequester = sidebarFocusRequester,\n                            onFocusMoved = {\n                                // 焦点返回评论列表\n                                focusOnSidebar = false\n                                scope.launch {\n                                    focusRequester.requestFocus(scope)\n                                }\n                            },\n                            focusRequestToken = sidebarFocusRequestToken\n                        )\n                    }\n\n                    // 右侧评论列表区域\n                    Column(\n                        modifier = Modifier\n                            .weight(1f)\n                            .fillMaxHeight()\n                            .onFocusChanged { focusState ->\n                                if (focusState.hasFocus && focusOnSidebar) {\n                                    focusOnSidebar = false\n                                }\n                            },\n                        verticalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        // 标题栏\n                        Row(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(bottom = 8.dp),\n                            horizontalArrangement = Arrangement.SpaceBetween,\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Column(\n                                verticalArrangement = Arrangement.spacedBy(4.dp)\n                            ) {\n                                Text(\n                                    text = \"评论\",\n                                    style = MaterialTheme.typography.titleLarge,\n                                    color = Color.White\n                                )\n                                // 操作提示\n                                Text(\n                                    text = if (showSidebar) \"左键返回顶部,返回键切换剧集\" else \"左键返回顶部\",\n                                    style = MaterialTheme.typography.bodySmall,\n                                    color = Color.White.copy(alpha = 0.5f)\n                                )\n                                // 显示当前剧集\n                                if (currentEpisode != null && showSidebar) {\n                                    Text(\n                                        text = generateEpisodeTitle(currentEpisode, \"\"),\n                                        style = MaterialTheme.typography.bodySmall,\n                                        color = Color.White.copy(alpha = 0.7f)\n                                    )\n                                }\n                            }\n                            Text(\n                                text = if (comments.isNotEmpty()) \"${comments.size} 条\" else \"\",\n                                style = MaterialTheme.typography.bodySmall,\n                                color = Color.White.copy(alpha = 0.7f)\n                            )\n                        }\n\n                        // 评论列表\n                        if (error != null) {\n                            Box(\n                                modifier = Modifier\n                                    .weight(1f)\n                                    .fillMaxWidth()\n                                    .focusRequester(focusRequester)\n                                    .focusable(),\n                                contentAlignment = Alignment.TopStart\n                            ) {\n                                Text(\n                                    text = error ?: \"加载失败\",\n                                    color = Color.Red,\n                                    modifier = Modifier\n                                        .align(Alignment.Center)\n                                        .padding(16.dp)\n                                )\n                            }\n                        } else if (comments.isEmpty() && !loading) {\n                            Box(\n                                modifier = Modifier\n                                    .weight(1f)\n                                    .fillMaxWidth()\n                                    .focusRequester(focusRequester)\n                                    .focusable(),\n                                contentAlignment = Alignment.TopStart\n                            ) {\n                                Text(\n                                    text = \"暂无评论\",\n                                    color = Color.White.copy(alpha = 0.5f),\n                                    modifier = Modifier\n                                        .align(Alignment.Center)\n                                        .padding(16.dp)\n                                )\n                            }\n                        } else {\n                            LazyColumn(\n                                modifier = Modifier\n                                    .weight(1f)\n                                    .fillMaxWidth()\n                                    .focusRequester(focusRequester)\n                                    .onPreviewKeyEvent { event ->\n                                        when {\n                                            // 左键：第一条时转移焦点到左侧边栏，否则返回顶部\n                                            event.isKeyDown() && event.isDpadLeft() -> {\n                                                if (focusedCommentIndex == 0 && showSidebar) {\n                                                    requestFocusToCurrentEpisode()\n                                                } else {\n                                                    scope.launch {\n                                                        listState.scrollToItem(0)\n                                                        delay(100)\n                                                        focusRequester.requestFocus(scope)\n                                                    }\n                                                }\n                                                true\n                                            }\n                                            // 下键：逐步滚动，在列表末尾时阻止焦点移出\n                                            event.isKeyDown() && event.isDpadDown() -> {\n                                                val layoutInfo = listState.layoutInfo\n\n                                                // 已聚焦到最后一条评论时，阻止焦点移出列表\n                                                if (focusedCommentIndex >= comments.size - 1 && comments.isNotEmpty()) {\n                                                    true\n                                                } else {\n                                                    val currentItemInfo = layoutInfo.visibleItemsInfo\n                                                        .firstOrNull { it.index == focusedCommentIndex }\n\n                                                    if (currentItemInfo != null) {\n                                                        val viewportEnd = layoutInfo.viewportEndOffset\n                                                        val itemBottom = currentItemInfo.offset + currentItemInfo.size\n\n                                                        // 如果评论底部不可见，逐步滚动\n                                                        if (itemBottom > viewportEnd) {\n                                                            scope.launch {\n                                                                // 每次滚动约 100dp\n                                                                val scrollAmount = with(density) { 100.dp.toPx() }\n                                                                listState.animateScrollBy(scrollAmount)\n                                                            }\n                                                            true // 拦截事件，不允许焦点转移\n                                                        } else {\n                                                            false // 评论已完全可见，允许焦点转移\n                                                        }\n                                                    } else {\n                                                        false\n                                                    }\n                                                }\n                                            }\n                                            else -> false\n                                        }\n                                    },\n                            state = listState,\n                            verticalArrangement = Arrangement.spacedBy(8.dp)\n                        ) {\n                            itemsIndexed(\n                                items = comments,\n                                key = { index, it -> \"$index-comment-${it.rpid}\" }\n                            ) { index, comment ->\n                                CommentItem(\n                                    comment = comment,\n                                    modifier = Modifier\n                                        .fillMaxWidth()\n                                        .onFocusChanged { focusState ->\n                                            if (focusState.hasFocus) {\n                                                focusedCommentIndex = index\n                                            }\n                                        },\n                                    onClick = {\n                                        // 只有有子评论时才能点击打开子评论浮窗\n                                        if (comment.repliesCount > 0) {\n                                            selectedCommentIndex = index\n                                            selectedRootComment = comment\n                                            showSubCommentPanel = true\n                                        }\n                                    },\n                                    onLongClick = {\n                                        if (comment.pictures.isNotEmpty()) {\n                                            imageViewerPictures = comment.pictures\n                                            showImageViewer = true\n                                        }\n                                    }\n                                )\n                            }\n\n                            // 加载状态\n                            if (loading) {\n                                item {\n                                    Row(\n                                        modifier = Modifier\n                                            .fillMaxWidth()\n                                            .padding(16.dp),\n                                        horizontalArrangement = Arrangement.Center\n                                    ) {\n                                        LoadingTip()\n                                    }\n                                }\n                            }\n\n                            // 没有更多了\n                            if (!hasNext && comments.isNotEmpty()) {\n                                item {\n                                    Text(\n                                        modifier = Modifier\n                                            .fillMaxWidth()\n                                            .padding(16.dp),\n                                        text = \"没有更多评论了\",\n                                        style = MaterialTheme.typography.bodySmall,\n                                        color = Color.White.copy(alpha = 0.5f)\n                                    )\n                                }\n                            }\n                        }\n                    }\n\n                    // 底部提示\n                    Spacer(modifier = Modifier.height(8.dp))\n                } // Column (右侧评论列表区域) 结束\n            } // Row 结束\n        } // Surface 结束\n        } // AnimatedVisibility 结束\n    } // Box 结束\n\n    // 子评论浮窗\n    if (selectedRootComment != null) {\n        SubCommentPanel(\n            show = showSubCommentPanel,\n            oid = currentOid,\n            rootId = selectedRootComment!!.rpid,\n            rootComment = selectedRootComment!!,\n            onHide = {\n                showSubCommentPanel = false\n                selectedRootComment = null\n            }\n        )\n    }\n\n    // 全屏图片查看器\n    if (showImageViewer && imageViewerPictures.isNotEmpty()) {\n        FullscreenImageViewer(\n            pictures = imageViewerPictures,\n            onDismiss = {\n                showImageViewer = false\n                imageViewerPictures = emptyList()\n            }\n        )\n    }\n}\n\n/**\n * 选集侧边栏项数据类\n */\nprivate data class EpisodeItem(\n    val episode: Episode,\n    val sectionTitle: String,\n    val index: Int\n)\n\n/**\n * 生成剧集标题\n */\nprivate fun generateEpisodeTitle(\n    episode: Episode?,\n    sectionTitle: String\n): String {\n    if (episode == null) return \"\"\n\n    return if (episode.longTitle.isNotEmpty()) {\n        runCatching {\n            \"第 ${episode.title.toInt()} 集 \"\n        }.getOrDefault(\"\") + episode.longTitle\n    } else if (sectionTitle == \"正片\") {\n        runCatching {\n            \"第 ${episode.title.toInt()} 集\"\n        }.getOrDefault(episode.title)\n    } else {\n        episode.title\n    }\n}\n\n/**\n * 选集侧边栏组件\n */\n@Composable\nprivate fun EpisodeSidebar(\n    episodes: List<EpisodeItem>,\n    currentEpisode: Episode?,\n    onEpisodeSelected: (EpisodeItem) -> Unit,\n    modifier: Modifier = Modifier,\n    focusRequester: FocusRequester,\n    onFocusMoved: () -> Unit = {},\n    focusRequestToken: Int = 0\n) {\n    val listState = rememberLazyListState()\n    val scope = rememberCoroutineScope()\n    val context = LocalContext.current\n\n    // 为每个剧集项创建独立的 FocusRequester\n    val itemFocusRequesters = remember(episodes.size) {\n        List(episodes.size) { FocusRequester() }\n    }\n\n    // 计算在 LazyColumn 中的实际索引（考虑章节标题）\n    fun calculateLazyColumnIndex(episodes: List<EpisodeItem>, episodeIndex: Int): Int {\n        if (episodeIndex < 0 || episodes.isEmpty()) return 0\n        var actualIndex = 0\n        var lastSectionTitle = \"\"\n        // 遍历到目标索引之前的所有项\n        for (i in 0 until episodeIndex) {\n            if (episodes[i].sectionTitle != lastSectionTitle) {\n                lastSectionTitle = episodes[i].sectionTitle\n                actualIndex++  // 章节标题占一个索引\n            }\n            actualIndex++  // 剧集项占一个索引\n        }\n        // 检查目标项本身是否有新章节标题\n        if (episodes[episodeIndex].sectionTitle != lastSectionTitle) {\n            actualIndex++  // 目标项的章节标题\n        }\n        return actualIndex  // 返回剧集项的正确索引\n    }\n\n    // 初始化时滚动到当前选中的剧集（只滚动，不请求焦点）\n    LaunchedEffect(currentEpisode, episodes) {\n        val index = episodes.indexOfFirst { it.episode.id == currentEpisode?.id }\n        if (index >= 0) {\n            val actualIndex = calculateLazyColumnIndex(episodes, index)\n            listState.scrollToItem(maxOf(0, actualIndex - 2))\n        }\n    }\n\n    // 当收到焦点请求时，滚动并请求焦点到当前剧集\n    LaunchedEffect(focusRequestToken) {\n        if (focusRequestToken > 0) {\n            delay(50) // 短暂等待确保布局就绪\n            val index = episodes.indexOfFirst { it.episode.id == currentEpisode?.id }\n            if (index >= 0) {\n                val actualIndex = calculateLazyColumnIndex(episodes, index)\n                listState.scrollToItem(maxOf(0, actualIndex - 2))\n                delay(50)\n                // 直接请求焦点到当前选中的剧集项\n                itemFocusRequesters.getOrNull(index)?.requestFocus()\n            } else {\n                // 没有找到当前剧集，焦点到第一个剧集\n                itemFocusRequesters.firstOrNull()?.requestFocus()\n            }\n        }\n    }\n\n    LazyColumn(\n        modifier = modifier.focusRequester(focusRequester),\n        state = listState,\n        verticalArrangement = Arrangement.spacedBy(4.dp),\n        contentPadding = PaddingValues(vertical = 8.dp)\n    ) {\n        // 按章节分组显示\n        var lastSectionTitle = \"\"\n\n        episodes.forEachIndexed { index, item ->\n            // 章节标题\n            if (item.sectionTitle != lastSectionTitle) {\n                lastSectionTitle = item.sectionTitle\n                item {\n                    Text(\n                        text = item.sectionTitle,\n                        style = MaterialTheme.typography.titleSmall,\n                        color = Color.White.copy(alpha = 0.7f),\n                        modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)\n                    )\n                }\n            }\n\n            // 剧集按钮\n            item {\n                val isSelected = item.episode.id == currentEpisode?.id\n                EpisodeSidebarItem(\n                    episode = item.episode,\n                    sectionTitle = item.sectionTitle,\n                    isSelected = isSelected,\n                    onClick = { onEpisodeSelected(item) },\n                    onBackKeyPressed = onFocusMoved,\n                    focusRequester = itemFocusRequesters[index]\n                )\n            }\n        }\n    }\n}\n\n/**\n * 选集侧边栏单项组件\n */\n@Composable\nprivate fun EpisodeSidebarItem(\n    episode: Episode,\n    sectionTitle: String,\n    isSelected: Boolean,\n    onClick: () -> Unit,\n    onBackKeyPressed: () -> Unit = {},\n    focusRequester: FocusRequester = remember { FocusRequester() }\n) {\n    val borderColor = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.3f) else null\n    val context = LocalContext.current\n\n    Surface(\n        modifier = Modifier.focusRequester(focusRequester),\n        onClick = onClick,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = if (isSelected) {\n                Color.White.copy(alpha = 0.15f)\n            } else {\n                Color.Transparent\n            },\n            focusedContainerColor = if (isSelected) {\n                Color.White.copy(alpha = 0.15f)\n            } else {\n                Color.Transparent\n            }\n        ),\n        scale = ClickableSurfaceDefaults.scale(\n            focusedScale = 1f\n        ),\n        border = ClickableSurfaceDefaults.border(\n            border = borderColor?.let {\n                Border(border = BorderStroke(width = 2.dp, color = it))\n            } ?: Border.None,\n            focusedBorder = Border(\n                border = BorderStroke(width = 2.dp, color = MaterialTheme.colorScheme.border),\n                shape = MaterialTheme.shapes.small\n            )\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.small)\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 8.dp, vertical = 4.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            // 剧集封面缩略图\n            AsyncImage(\n                modifier = Modifier\n                    .size(48.dp)\n                    .clip(MaterialTheme.shapes.extraSmall),\n                model = episode.cover.resizedImageUrl(ImageSize.UgcEpisodeCover),\n                contentDescription = null,\n                contentScale = ContentScale.Crop\n            )\n\n            // 剧集标题\n            Column(\n                modifier = Modifier.weight(1f),\n                verticalArrangement = Arrangement.spacedBy(2.dp)\n            ) {\n                Text(\n                    text = generateEpisodeTitle(episode, sectionTitle),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = Color.White,\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis\n                )\n                if (episode.longTitle.isNotEmpty()) {\n                    Text(\n                        text = episode.longTitle,\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White.copy(alpha = 0.6f),\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/DescriptionPanel.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.gestures.animateScrollBy\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.ExperimentalTvMaterial3Api\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.SuggestionChip\nimport androidx.tv.material3.SurfaceDefaults\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.video.Tag\nimport dev.aaa1115910.bv.util.isDpadDown\nimport dev.aaa1115910.bv.util.isDpadUp\nimport dev.aaa1115910.bv.util.isKeyDown\nimport dev.aaa1115910.bv.util.onBackPressed\nimport dev.aaa1115910.bv.util.requestFocus\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\n/**\n * 视频简介浮层组件\n *\n * @param show 是否显示浮层\n * @param description 视频简介\n * @param tags 视频标签列表\n * @param onHide 关闭浮层回调\n * @param onClickTag 标签点击回调\n */\n@OptIn(ExperimentalTvMaterial3Api::class)\n@Composable\nfun DescriptionPanel(\n    show: Boolean,\n    description: String,\n    tags: List<Tag> = emptyList(),\n    onHide: () -> Unit,\n    onClickTag: (Tag) -> Unit = {}\n) {\n    val scope = rememberCoroutineScope()\n    val contentFocusRequester = remember { FocusRequester() }\n    val firstTagFocusRequester = remember { FocusRequester() }\n    val scrollState = rememberScrollState()\n    val density = LocalDensity.current\n    var hasRequestedFocus by remember { mutableStateOf(false) }\n\n    // 显示时请求焦点：优先聚焦第一个标签，没有标签则聚焦简介内容\n    LaunchedEffect(show) {\n        if (show) {\n            delay(300)\n            if (tags.isNotEmpty()) {\n                firstTagFocusRequester.requestFocus(scope)\n            } else {\n                contentFocusRequester.requestFocus(scope)\n            }\n            hasRequestedFocus = true\n        } else {\n            hasRequestedFocus = false\n        }\n    }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .clickable(onClick = onHide),\n        contentAlignment = Alignment.CenterEnd\n    ) {\n        AnimatedVisibility(\n            visible = show,\n            enter = expandHorizontally(expandFrom = Alignment.End),\n            exit = shrinkHorizontally(shrinkTowards = Alignment.End)\n        ) {\n            Surface(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .padding(horizontal = 16.dp, vertical = 16.dp)\n                    .widthIn(min = 300.dp, max = 400.dp)\n                    .fillMaxWidth(0.3f)\n                    .clickable(enabled = true, onClick = {}) // 阻止点击穿透\n                    .onBackPressed { onHide() },\n                colors = SurfaceDefaults.colors(\n                    containerColor = Color.Black.copy(alpha = 0.85f)\n                ),\n                shape = MaterialTheme.shapes.large\n            ) {\n                Column(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .padding(16.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    // 标题栏（固定不滚动）\n                    Text(\n                        text = \"视频简介\",\n                        style = MaterialTheme.typography.titleLarge,\n                        color = Color.White,\n                        modifier = Modifier.padding(bottom = 4.dp)\n                    )\n\n                    // 标签行（固定不滚动，可横向滚动）\n                    if (tags.isNotEmpty()) {\n                        LazyRow(\n                            modifier = Modifier.fillMaxWidth(),\n                            contentPadding = PaddingValues(horizontal = 4.dp),\n                            horizontalArrangement = Arrangement.spacedBy(6.dp)\n                        ) {\n                            itemsIndexed(\n                                items = tags,\n                                key = { index, tag -> \"$index-desc-tag-${tag.name}\" }\n                            ) { index, tag ->\n                                SuggestionChip(\n                                    modifier = if (index == 0) Modifier.focusRequester(firstTagFocusRequester) else Modifier,\n                                    onClick = { onClickTag(tag) }\n                                ) {\n                                    Text(text = tag.name)\n                                }\n                            }\n                        }\n                    }\n\n                    // 简介内容（可滚动）\n                    Box(\n                        modifier = Modifier\n                            .weight(1f)\n                            .fillMaxWidth()\n                            .focusRequester(contentFocusRequester)\n                            .focusable()\n                            .onPreviewKeyEvent { event ->\n                                when {\n                                    event.isKeyDown() && event.isDpadDown() -> {\n                                        scope.launch {\n                                            val scrollAmount =\n                                                with(density) { 100.dp.toPx() }\n                                            scrollState.animateScrollBy(scrollAmount)\n                                        }\n                                        true\n                                    }\n\n                                    event.isKeyDown() && event.isDpadUp() -> {\n                                        scope.launch {\n                                            val scrollAmount =\n                                                with(density) { 100.dp.toPx() }\n                                            scrollState.animateScrollBy(-scrollAmount)\n                                        }\n                                        true\n                                    }\n\n                                    else -> false\n                                }\n                            }\n                    ) {\n                        Text(\n                            modifier = Modifier\n                                .verticalScroll(scrollState)\n                                .padding(top = 4.dp),\n                            text = description,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = Color.White.copy(alpha = 0.9f)\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/FullscreenImageViewer.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport coil.compose.AsyncImagePainter\nimport coil.compose.rememberAsyncImagePainter\nimport dev.aaa1115910.biliapi.entity.Picture\nimport dev.aaa1115910.bv.util.isDpadLeft\nimport dev.aaa1115910.bv.util.isDpadRight\nimport dev.aaa1115910.bv.util.isKeyDown\n\n/**\n * 全屏图片查看器\n *\n * @param pictures 图片列表\n * @param initialIndex 初始显示的图片索引\n * @param onDismiss 关闭回调\n */\n@Composable\nfun FullscreenImageViewer(\n    pictures: List<Picture>,\n    initialIndex: Int = 0,\n    onDismiss: () -> Unit\n) {\n    var currentIndex by remember { mutableIntStateOf(initialIndex) }\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus()\n    }\n\n    Dialog(\n        onDismissRequest = onDismiss,\n        properties = DialogProperties(\n            usePlatformDefaultWidth = false,\n            dismissOnBackPress = true\n        )\n    ) {\n        Surface(\n            modifier = Modifier\n                .fillMaxSize()\n                .focusRequester(focusRequester)\n                .onPreviewKeyEvent { event ->\n                    if (!event.isKeyDown()) return@onPreviewKeyEvent false\n                    when {\n                        event.isDpadLeft() -> {\n                            if (currentIndex > 0) currentIndex--\n                            true\n                        }\n                        event.isDpadRight() -> {\n                            if (currentIndex < pictures.size - 1) currentIndex++\n                            true\n                        }\n                        event.key == Key.Back -> {\n                            onDismiss()\n                            true\n                        }\n                        else -> false\n                    }\n                },\n            onClick = { /* 消费点击事件 */ },\n            colors = ClickableSurfaceDefaults.colors(\n                containerColor = Color.Black,\n                focusedContainerColor = Color.Black,\n                pressedContainerColor = Color.Black\n            ),\n            scale = ClickableSurfaceDefaults.scale(focusedScale = 1f, pressedScale = 1f),\n            shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(0.dp))\n        ) {\n            Box(\n                modifier = Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center\n            ) {\n                // 图片\n                val painter = rememberAsyncImagePainter(model = pictures[currentIndex].url)\n                val painterState = painter.state\n\n                if (painterState is AsyncImagePainter.State.Loading) {\n                    CircularProgressIndicator(\n                        modifier = Modifier.size(36.dp),\n                        color = Color.White\n                    )\n                }\n\n                AsyncImage(\n                    model = pictures[currentIndex].url,\n                    contentDescription = null,\n                    modifier = Modifier.fillMaxSize(),\n                    contentScale = ContentScale.Fit\n                )\n\n                // 页码指示器\n                Box(\n                    modifier = Modifier\n                        .align(Alignment.BottomCenter)\n                        .padding(bottom = 32.dp)\n                        .background(\n                            Color.Black.copy(alpha = 0.6f),\n                            RoundedCornerShape(16.dp)\n                        )\n                        .padding(horizontal = 16.dp, vertical = 8.dp)\n                ) {\n                    Text(\n                        text = \"${currentIndex + 1}/${pictures.size}\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = Color.White\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/GeetestTvVerifyDialog.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.DashPathEffect\nimport android.graphics.Paint\nimport android.os.SystemClock\nimport android.view.KeyEvent\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.ViewGroup\nimport android.webkit.JavascriptInterface\nimport android.webkit.WebChromeClient\nimport android.webkit.WebView\nimport android.widget.FrameLayout\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\nprivate val logger = KotlinLogging.logger(\"GeetestTvVerify\")\n\n/**\n * Geetest 验证结果\n */\ndata class GeetestTvResult(\n    val challenge: String,\n    val validate: String,\n    val seccode: String,\n)\n\n/**\n * TV 端 Geetest 风控验证弹窗\n *\n * 在 WebView 中加载极验 JS SDK，上层覆盖十字光标，\n * 通过遥控器方向键移动光标、确认键触发点击 → 将 touch 事件注入 WebView。\n *\n * 交互方式：\n * - 方向键 (D-Pad) 移动十字光标，长按/连按加速\n * - 确认键 (Center/Enter) 在当前光标位置点击 WebView\n * - 返回键 (Back) 取消验证\n *\n * @param gt       Geetest gt 参数\n * @param challenge Geetest challenge 参数\n * @param onResult  验证成功后回调\n * @param onDismiss 用户取消/关闭时回调\n */\n@Composable\nfun GeetestTvVerifyDialog(\n    gt: String,\n    challenge: String,\n    onResult: (GeetestTvResult) -> Unit,\n    onDismiss: () -> Unit,\n) {\n    Dialog(\n        onDismissRequest = onDismiss,\n        properties = DialogProperties(\n            usePlatformDefaultWidth = false,\n            dismissOnBackPress = false,\n        )\n    ) {\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .background(Color.Black.copy(alpha = 0.75f)),\n            contentAlignment = Alignment.Center,\n        ) {\n            GeetestTvVerifyContent(\n                gt = gt,\n                challenge = challenge,\n                onResult = onResult,\n                onDismiss = onDismiss,\n            )\n        }\n    }\n}\n\n@SuppressLint(\"SetJavaScriptEnabled\")\n@Composable\nprivate fun GeetestTvVerifyContent(\n    gt: String,\n    challenge: String,\n    onResult: (GeetestTvResult) -> Unit,\n    onDismiss: () -> Unit,\n) {\n    val density = LocalDensity.current\n    val focusRequester = remember { FocusRequester() }\n\n    // WebView 容器实际像素尺寸\n    var containerWidthPx by remember { mutableFloatStateOf(0f) }\n    var containerHeightPx by remember { mutableFloatStateOf(0f) }\n\n    // 光标位置 (像素坐标，相对于 WebView 容器)\n    var cursorX by remember { mutableFloatStateOf(0f) }\n    var cursorY by remember { mutableFloatStateOf(0f) }\n    var cursorInitialized by remember { mutableStateOf(false) }\n\n    // 状态提示\n    var statusText by remember { mutableStateOf(\"正在加载验证码…\") }\n\n    // WebView 引用\n    var webViewRef by remember { mutableStateOf<WebView?>(null) }\n\n    // 光标覆盖层引用\n    var overlayRef by remember { mutableStateOf<CrosshairOverlayView?>(null) }\n\n    // 移动步长\n    val baseStep = with(density) { 6.dp.toPx() }\n    val fastStep = with(density) { 20.dp.toPx() }\n\n    // 初始化光标到中心\n    LaunchedEffect(containerWidthPx, containerHeightPx) {\n        if (containerWidthPx > 0 && containerHeightPx > 0 && !cursorInitialized) {\n            cursorX = containerWidthPx / 2f\n            cursorY = containerHeightPx / 2f\n            cursorInitialized = true\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus()\n    }\n\n    // 清理 WebView\n    DisposableEffect(Unit) {\n        onDispose {\n            webViewRef?.let { wv ->\n                runCatching {\n                    wv.removeJavascriptInterface(\"Android\")\n                    wv.stopLoading()\n                    wv.destroy()\n                }\n            }\n        }\n    }\n\n    fun clampCursor() {\n        cursorX = cursorX.coerceIn(0f, containerWidthPx)\n        cursorY = cursorY.coerceIn(0f, containerHeightPx)\n    }\n\n    fun moveCursor(dx: Float, dy: Float, fast: Boolean) {\n        val step = if (fast) fastStep else baseStep\n        cursorX += dx * step\n        cursorY += dy * step\n        clampCursor()\n    }\n\n    fun dispatchClickToWebView() {\n        val wv = webViewRef ?: return\n        val now = SystemClock.uptimeMillis()\n        val x = cursorX\n        val y = cursorY\n        val down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, x, y, 0)\n        val up = MotionEvent.obtain(now, now + 50, MotionEvent.ACTION_UP, x, y, 0)\n        wv.dispatchTouchEvent(down)\n        wv.dispatchTouchEvent(up)\n        down.recycle()\n        up.recycle()\n        logger.debug { \"Dispatched click at ($x, $y)\" }\n    }\n\n    Column(\n        modifier = Modifier\n            .fillMaxWidth(0.45f)\n            .clip(RoundedCornerShape(16.dp))\n            .background(MaterialTheme.colorScheme.surface)\n            .onPreviewKeyEvent { event ->\n                val isDown = event.nativeKeyEvent.action == KeyEvent.ACTION_DOWN\n                val isLongPress = event.nativeKeyEvent.repeatCount > 0\n                val keyCode = event.nativeKeyEvent.keyCode\n\n                if (!isDown) return@onPreviewKeyEvent false\n\n                when (keyCode) {\n                    KeyEvent.KEYCODE_DPAD_UP -> {\n                        moveCursor(0f, -1f, isLongPress); true\n                    }\n\n                    KeyEvent.KEYCODE_DPAD_DOWN -> {\n                        moveCursor(0f, 1f, isLongPress); true\n                    }\n\n                    KeyEvent.KEYCODE_DPAD_LEFT -> {\n                        moveCursor(-1f, 0f, isLongPress); true\n                    }\n\n                    KeyEvent.KEYCODE_DPAD_RIGHT -> {\n                        moveCursor(1f, 0f, isLongPress); true\n                    }\n\n                    KeyEvent.KEYCODE_DPAD_CENTER,\n                    KeyEvent.KEYCODE_ENTER,\n                    KeyEvent.KEYCODE_NUMPAD_ENTER -> {\n                        dispatchClickToWebView(); true\n                    }\n\n                    KeyEvent.KEYCODE_BACK -> {\n                        onDismiss(); true\n                    }\n\n                    else -> false\n                }\n            }\n            .focusRequester(focusRequester)\n            .focusable(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        // ---- 状态提示 ----\n        Text(\n            text = statusText,\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp, vertical = 10.dp),\n            style = MaterialTheme.typography.titleSmall,\n            color = MaterialTheme.colorScheme.onSurface,\n            textAlign = TextAlign.Center,\n        )\n\n        // ---- WebView + 十字光标叠加 ----\n        Box(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 12.dp)\n                .padding(bottom = 4.dp),\n        ) {\n            AndroidView(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .clip(RoundedCornerShape(8.dp)),\n                factory = { ctx ->\n                    val webView = WebView(ctx).apply {\n                        layoutParams = FrameLayout.LayoutParams(\n                            ViewGroup.LayoutParams.MATCH_PARENT,\n                            ViewGroup.LayoutParams.WRAP_CONTENT,\n                        )\n                        settings.javaScriptEnabled = true\n                        settings.domStorageEnabled = true\n                        settings.mediaPlaybackRequiresUserGesture = false\n                        isFocusable = false\n                        isFocusableInTouchMode = false\n                        webChromeClient = WebChromeClient()\n\n                        addJavascriptInterface(\n                            object {\n                                @JavascriptInterface\n                                fun onGeetestResult(\n                                    validate: String?,\n                                    seccode: String?,\n                                    geetestChallenge: String?,\n                                ) {\n                                    val v = validate.orEmpty().trim()\n                                    val s = seccode.orEmpty().trim()\n                                    val c = geetestChallenge.orEmpty().trim()\n                                    if (v.isBlank() || s.isBlank() || c.isBlank()) return\n                                    logger.info { \"Geetest verification succeeded\" }\n                                    onResult(\n                                        GeetestTvResult(\n                                            challenge = c,\n                                            validate = v,\n                                            seccode = s\n                                        )\n                                    )\n                                }\n\n                                @JavascriptInterface\n                                fun onStatusUpdate(text: String?) {\n                                    text?.let { statusText = it }\n                                }\n                            },\n                            \"Android\"\n                        )\n\n                        loadDataWithBaseURL(\n                            \"https://api.bilibili.com/\",\n                            buildGeetestHtml(gt, challenge),\n                            \"text/html\",\n                            \"utf-8\",\n                            null,\n                        )\n\n                        webViewRef = this\n                    }\n\n                    val overlay = CrosshairOverlayView(ctx).apply {\n                        layoutParams = FrameLayout.LayoutParams(\n                            ViewGroup.LayoutParams.MATCH_PARENT,\n                            ViewGroup.LayoutParams.MATCH_PARENT,\n                        )\n                        overlayRef = this\n                    }\n\n                    FrameLayout(ctx).apply {\n                        layoutParams = ViewGroup.LayoutParams(\n                            ViewGroup.LayoutParams.MATCH_PARENT,\n                            ViewGroup.LayoutParams.WRAP_CONTENT,\n                        )\n                        addView(webView)\n                        addView(overlay)\n                    }\n                },\n                update = { frame ->\n                    val wv = webViewRef ?: return@AndroidView\n                    wv.post {\n                        if (wv.width > 0 && wv.height > 0) {\n                            containerWidthPx = wv.width.toFloat()\n                            containerHeightPx = wv.height.toFloat()\n                        }\n                    }\n                    // 更新覆盖层光标位置\n                    overlayRef?.setCursorPosition(cursorX, cursorY)\n                },\n            )\n        }\n\n        // ---- 操作提示 ----\n        Text(\n            text = \"方向键移动光标 ｜ 确认键点击 ｜ 返回键取消\",\n            modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),\n            style = MaterialTheme.typography.labelSmall,\n            color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),\n            textAlign = TextAlign.Center,\n        )\n    }\n}\n\n/**\n * 原生 View 十字光标覆盖层，渲染在 WebView 之上。\n */\nprivate class CrosshairOverlayView(context: Context) : View(context) {\n    private var cx = 0f\n    private var cy = 0f\n\n    private val density = context.resources.displayMetrics.density\n    private val armLen = 18f * density\n    private val gap = 5f * density\n    private val strokeW = 2f * density\n    private val shadowW = 3.5f * density\n    private val dotRadius = 3f * density\n    private val dotShadowRadius = 4f * density\n    private val circleRadius = armLen + gap\n\n    private val whitePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        color = 0xFFFFFFFF.toInt()\n        style = Paint.Style.STROKE\n        strokeWidth = strokeW\n    }\n    private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        color = 0x99000000.toInt()\n        style = Paint.Style.STROKE\n        strokeWidth = shadowW\n    }\n    private val dotWhitePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        color = 0xFFFFFFFF.toInt()\n        style = Paint.Style.FILL\n    }\n    private val dotShadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        color = 0x99000000.toInt()\n        style = Paint.Style.FILL\n    }\n    private val dashPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        color = 0xB3FFFFFF.toInt()\n        style = Paint.Style.STROKE\n        strokeWidth = 1f * density\n        pathEffect = DashPathEffect(floatArrayOf(6f * density, 4f * density), 0f)\n    }\n\n    fun setCursorPosition(x: Float, y: Float) {\n        cx = x\n        cy = y\n        invalidate()\n    }\n\n    override fun onDraw(canvas: Canvas) {\n        super.onDraw(canvas)\n        if (cx == 0f && cy == 0f) return\n\n        // 阴影线\n        canvas.drawLine(cx, cy - gap - armLen, cx, cy - gap, shadowPaint)\n        canvas.drawLine(cx, cy + gap, cx, cy + gap + armLen, shadowPaint)\n        canvas.drawLine(cx - gap - armLen, cy, cx - gap, cy, shadowPaint)\n        canvas.drawLine(cx + gap, cy, cx + gap + armLen, cy, shadowPaint)\n\n        // 白色十字\n        canvas.drawLine(cx, cy - gap - armLen, cx, cy - gap, whitePaint)\n        canvas.drawLine(cx, cy + gap, cx, cy + gap + armLen, whitePaint)\n        canvas.drawLine(cx - gap - armLen, cy, cx - gap, cy, whitePaint)\n        canvas.drawLine(cx + gap, cy, cx + gap + armLen, cy, whitePaint)\n\n        // 中心点\n        canvas.drawCircle(cx, cy, dotShadowRadius, dotShadowPaint)\n        canvas.drawCircle(cx, cy, dotRadius, dotWhitePaint)\n\n        // 外圈虚线\n        canvas.drawCircle(cx, cy, circleRadius, dashPaint)\n    }\n}\n\n/**\n * 构建加载极验验证码的 HTML 页面。\n *\n * 使用极验 JS SDK (`gt.js`) 初始化验证并自动弹出验证窗口，\n * 用户在 WebView 中完成点选后，通过 JS bridge 回调原生代码。\n */\nprivate fun buildGeetestHtml(gt: String, challenge: String): String {\n    // 防止参数注入\n    val safeGt = gt.replace(\"\\\\\", \"\\\\\\\\\").replace(\"'\", \"\\\\'\").replace(\"<\", \"&lt;\")\n    val safeChallenge = challenge.replace(\"\\\\\", \"\\\\\\\\\").replace(\"'\", \"\\\\'\").replace(\"<\", \"&lt;\")\n    return \"\"\"\n<!DOCTYPE html>\n<html>\n<head>\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no\"/>\n  <script src=\"https://static.geetest.com/static/tools/gt.js\"></script>\n  <style>\n    * { margin: 0; padding: 0; box-sizing: border-box; }\n    html, body {\n      width: 100%;\n      background: transparent;\n      font-family: sans-serif;\n      overflow: hidden;\n    }\n    body {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      padding: 4px;\n    }\n    #captcha {\n      width: 100%;\n      min-height: 300px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n    }\n    /* 让极验验证面板自适应容器 */\n    .geetest_panel { position: relative !important; }\n    .geetest_panel_box { position: relative !important; }\n    .geetest_wind { position: relative !important; }\n    /* 隐藏极验自带的关闭按钮，用遥控器返回键代替 */\n    .geetest_panel_ghost,\n    .geetest_close { display: none !important; }\n  </style>\n</head>\n<body>\n  <div id=\"captcha\"></div>\n  <script>\n    (function() {\n      function notify(msg) {\n        try { window.Android.onStatusUpdate(msg); } catch(e) {}\n      }\n      if (typeof initGeetest !== 'function') {\n        notify('验证码脚本加载失败，请检查网络');\n        return;\n      }\n      notify('正在初始化验证…');\n      initGeetest({\n        gt: '$safeGt',\n        challenge: '$safeChallenge',\n        new_captcha: true,\n        product: 'bind',\n        offline: false,\n        https: true\n      }, function(captchaObj) {\n        captchaObj.appendTo('#captcha');\n        captchaObj.onReady(function() {\n          notify('请使用方向键移动光标，确认键点击');\n          // 自动弹出验证\n          captchaObj.verify();\n        });\n        captchaObj.onSuccess(function() {\n          var res = captchaObj.getValidate();\n          if (!res) return;\n          notify('验证成功，正在提交…');\n          try {\n            window.Android.onGeetestResult(\n              res.geetest_validate,\n              res.geetest_seccode,\n              res.geetest_challenge\n            );\n          } catch(e) {}\n        });\n        captchaObj.onError(function(e) {\n          notify('验证出错：' + (e && (e.msg || e.error_code) || '未知错误'));\n        });\n        captchaObj.onClose(function() {\n          // 极验面板关闭后重新弹出，防止误触关闭\n          notify('验证已关闭，正在重新打开…');\n          setTimeout(function() { captchaObj.verify(); }, 500);\n        });\n      });\n    })();\n  </script>\n</body>\n</html>\n    \"\"\".trimIndent()\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/LibVLCDownloaderDialog.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.network.VlcLibsApi\nimport dev.aaa1115910.bv.player.BuildConfig\nimport dev.aaa1115910.bv.util.toast\nimport io.ktor.client.content.ProgressListener\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.File\nimport java.util.UUID\nimport java.util.zip.ZipInputStream\n\n@Composable\nfun LibVLCDownloaderDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    var processing by remember { mutableStateOf(false) }\n    var text by remember { mutableStateOf(\"等待操作中...\") }\n\n    val unZipLibs: (zipFile: File) -> Unit = { zipFile ->\n        val vlcLibsDir = File(context.filesDir, \"vlc_libs\")\n        vlcLibsDir.mkdir()\n        vlcLibsDir.listFiles()?.forEach { file ->\n            file.delete()\n        }\n\n        ZipInputStream(zipFile.inputStream())\n            .use { zipInputStream ->\n                generateSequence { zipInputStream.nextEntry }\n                    .map {\n                        UnzippedFile(\n                            filename = it.name,\n                            content = zipInputStream.readBytes()\n                        )\n                    }.toList()\n            }\n            .forEach {\n                println(\"Extracting ${it.filename}\")\n                val file = File(vlcLibsDir, it.filename)\n                file.createNewFile()\n                file.writeBytes(it.content)\n            }\n    }\n\n    val startInstall: () -> Unit = {\n        processing = true\n\n        scope.launch(Dispatchers.IO) {\n            runCatching {\n                text = \"正在获取下载地址\"\n                val release =\n                    VlcLibsApi.getRelease(BuildConfig.libVLCVersion)\n                        ?: throw IllegalStateException(\"Release not found\")\n                val tempFilename = \"${UUID.randomUUID()}.zip\"\n                val tempDir = File(context.cacheDir, \"libvlc_downloader\")\n                if (!tempDir.exists()) tempDir.mkdirs()\n                val tempFile = File(tempDir, tempFilename)\n                tempFile.createNewFile()\n\n                VlcLibsApi.downloadFile(\n                    release,\n                    tempFile,\n                    object : ProgressListener {\n                        override suspend fun onProgress(downloaded: Long, total: Long?) {\n                            text = \"正在下载(${downloaded / (total?.toFloat() ?: 0f) * 100}%)\"\n                        }\n                    })\n\n                text = \"正在解压\"\n                unZipLibs(tempFile)\n            }.onSuccess {\n                text = \"安装完成\"\n                onHideDialog()\n                withContext(Dispatchers.Main) {\n                    \"LibVLC 安装成功\".toast(context)\n                }\n            }.onFailure {\n                text = \"安装失败\"\n                withContext(Dispatchers.Main) {\n                    \"LibVLC 安装失败: ${it.message}\".toast(context)\n                }\n                it.printStackTrace()\n            }\n\n            processing = false\n        }\n    }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            title = { Text(text = \"LibVLC 下载器\") },\n            text = { Text(text = text) },\n            onDismissRequest = { if (!processing) onHideDialog() },\n            confirmButton = {\n                Button(\n                    onClick = { startInstall() },\n                    enabled = !processing\n                ) {\n                    Text(text = \"下载\")\n                }\n            },\n            dismissButton = {\n                OutlinedButton(\n                    onClick = { onHideDialog() },\n                    enabled = !processing\n                ) {\n                    Text(text = \"取消\")\n                }\n            }\n        )\n    }\n}\n\n@Suppress(\"ArrayInDataClass\")\nprivate data class UnzippedFile(val filename: String, val content: ByteArray)\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/LoadingTip.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun LoadingTip(\n    modifier: Modifier = Modifier\n) {\n    Row(\n        modifier = modifier,\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        CircularProgressIndicator(\n            modifier = Modifier\n                .size(36.dp)\n                .padding(8.dp),\n            color = MaterialTheme.colorScheme.onSurface,\n            strokeWidth = 2.dp\n        )\n        Text(text = stringResource(id = R.string.loading), fontSize = 22.sp)\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun LoadingTipPreview() {\n    BVTheme {\n        LoadingTip()\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/RemoteControlPanelDemo.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.constraintlayout.compose.ConstraintLayout\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.requestFocus\n\n@Composable\nfun RemoteControlPanelInfo() {\n    ConstraintLayout {\n        val (panelBorder, dPadBorder, centerBorder, backBorder) = createRefs()\n        val borderWidth = 3.dp\n        val contentColor = MaterialTheme.colorScheme.onSurface\n        val tipTextStyle = MaterialTheme.typography.labelLarge.copy(\n            fontSize = 24.sp,\n            lineHeight = 32.sp,\n            color = contentColor\n        )\n\n        Box(\n            modifier = Modifier\n                .constrainAs(panelBorder) {\n                    centerTo(parent)\n                }\n                .size(200.dp, 400.dp)\n                .border(borderWidth, contentColor, RoundedCornerShape(100.dp))\n        ) {}\n        Box(\n            modifier = Modifier\n                .constrainAs(dPadBorder) {\n                    top.linkTo(panelBorder.top, 8.dp)\n                    start.linkTo(panelBorder.start)\n                    end.linkTo(panelBorder.end)\n                }\n                .size(180.dp)\n                .border(borderWidth, contentColor, CircleShape)\n        ) {}\n        Box(\n            modifier = Modifier\n                .constrainAs(centerBorder) {\n                    centerTo(dPadBorder)\n                }\n                .size(70.dp)\n                .border(borderWidth, contentColor, CircleShape)\n        ) {}\n        Box(\n            modifier = Modifier\n                .constrainAs(backBorder) {\n                    top.linkTo(dPadBorder.bottom, 16.dp)\n                    start.linkTo(panelBorder.start, 16.dp)\n                }\n                .size(70.dp)\n                .border(borderWidth, contentColor, CircleShape),\n            contentAlignment = Alignment.Center\n        ) {\n            Icon(\n                modifier = Modifier.size(40.dp),\n                imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                contentDescription = null,\n                tint = contentColor\n            )\n        }\n\n        val (tipLineBack, tipBack) = createRefs()\n        Spacer(\n            modifier = Modifier\n                .constrainAs(tipLineBack) {\n                    end.linkTo(backBorder.start)\n                    top.linkTo(backBorder.top)\n                    bottom.linkTo(backBorder.bottom)\n                }\n                .width(80.dp)\n                .height(borderWidth / 2)\n                .background(contentColor)\n        )\n        Text(\n            modifier = Modifier\n                .constrainAs(tipBack) {\n                    top.linkTo(tipLineBack.top)\n                    bottom.linkTo(tipLineBack.bottom)\n                    end.linkTo(tipLineBack.start, 8.dp)\n                },\n            text = stringResource(R.string.remote_control_panel_demo_tip_back),\n            style = tipTextStyle\n        )\n\n        val (tipLineCenter, tipCenter) = createRefs()\n        Spacer(\n            modifier = Modifier\n                .constrainAs(tipLineCenter) {\n                    start.linkTo(centerBorder.end)\n                    top.linkTo(centerBorder.top)\n                    bottom.linkTo(centerBorder.bottom)\n                }\n                .width(125.dp)\n                .height(borderWidth / 2)\n                .background(contentColor)\n        )\n        Text(\n            modifier = Modifier\n                .constrainAs(tipCenter) {\n                    top.linkTo(tipLineCenter.top)\n                    bottom.linkTo(tipLineCenter.bottom)\n                    start.linkTo(tipLineCenter.end, 8.dp)\n                },\n            text = stringResource(R.string.remote_control_panel_demo_tip_center),\n            style = tipTextStyle\n        )\n\n        val (tipLineUp, tipUp) = createRefs()\n        Spacer(\n            modifier = Modifier\n                .constrainAs(tipLineUp) {\n                    start.linkTo(dPadBorder.end, (-80).dp)\n                    top.linkTo(dPadBorder.top, 20.dp)\n                }\n                .width(150.dp)\n                .height(borderWidth / 2)\n                .background(contentColor)\n        )\n        Text(\n            modifier = Modifier\n                .constrainAs(tipUp) {\n                    top.linkTo(tipLineUp.top)\n                    bottom.linkTo(tipLineUp.bottom)\n                    start.linkTo(tipLineUp.end, 8.dp)\n                },\n            text = stringResource(R.string.remote_control_panel_demo_tip_up),\n            style = tipTextStyle\n        )\n\n        val (tipLineDown, tipDown) = createRefs()\n        Spacer(\n            modifier = Modifier\n                .constrainAs(tipLineDown) {\n                    start.linkTo(dPadBorder.end, (-80).dp)\n                    bottom.linkTo(dPadBorder.bottom, 20.dp)\n                }\n                .width(150.dp)\n                .height(borderWidth / 2)\n                .background(contentColor)\n        )\n        Text(\n            modifier = Modifier\n                .constrainAs(tipDown) {\n                    top.linkTo(tipLineDown.top)\n                    bottom.linkTo(tipLineDown.bottom)\n                    start.linkTo(tipLineDown.end, 8.dp)\n                },\n            text = stringResource(R.string.remote_control_panel_demo_tip_down),\n            style = tipTextStyle\n        )\n\n        val (tipLineLR1, tipLineLR2, tipLineLR3, tipLR) = createRefs()\n        Spacer(\n            modifier = Modifier\n                .constrainAs(tipLineLR1) {\n                    end.linkTo(dPadBorder.end, 20.dp)\n                    top.linkTo(dPadBorder.top, 60.dp)\n                }\n                .rotate(45f)\n                .width(40.dp)\n                .height(borderWidth / 2)\n                .background(contentColor)\n        )\n        Spacer(\n            modifier = Modifier\n                .constrainAs(tipLineLR3) {\n                    end.linkTo(tipLineLR1.end, 120.dp)\n                    top.linkTo(tipLineLR1.top)\n                }\n                .rotate(45f)\n                .width(40.dp)\n                .height(borderWidth / 2)\n                .background(contentColor)\n        )\n        Spacer(\n            modifier = Modifier\n                .constrainAs(tipLineLR2) {\n                    end.linkTo(tipLineLR1.start, (-7).dp)\n                    top.linkTo(tipLineLR1.top, (-14).dp)\n                }\n                .width(200.dp)\n                .height(borderWidth / 2)\n                .background(contentColor)\n        )\n        Text(\n            modifier = Modifier\n                .constrainAs(tipLR) {\n                    top.linkTo(tipLineLR2.top)\n                    bottom.linkTo(tipLineLR2.bottom)\n                    end.linkTo(tipLineLR2.start, 8.dp)\n                },\n            text = stringResource(R.string.remote_control_panel_demo_tip_lr),\n            style = tipTextStyle\n        )\n    }\n}\n\n@Composable\nfun RemoteControlPanelDemo(\n    modifier: Modifier = Modifier,\n    onConfirm: () -> Unit = {}\n) {\n    val focusRequester = remember { FocusRequester() }\n\n    val scope = rememberCoroutineScope()\n\n    LaunchedEffect(Unit) {\n        runCatching {\n            focusRequester.requestFocus(scope)\n        }\n    }\n\n    Surface(\n        modifier = modifier\n    ) {\n        Box(\n            modifier = Modifier\n                .focusRequester(focusRequester)\n                .fillMaxSize()\n                .clickable { onConfirm() },\n            contentAlignment = Alignment.Center\n        ) {\n            Column(\n                horizontalAlignment = Alignment.CenterHorizontally,\n                verticalArrangement = Arrangement.spacedBy(16.dp)\n            ) {\n                RemoteControlPanelInfo()\n                Text(\n                    text = stringResource(R.string.remote_control_panel_demo_tip_bottom),\n                    color = MaterialTheme.colorScheme.onSurface\n                )\n            }\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun RemoteControlPanelInfoPreview() {\n    BVTheme {\n        Surface {\n            RemoteControlPanelInfo()\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun RemoteControlPanelDemoPreview() {\n    BVTheme {\n        Surface {\n            RemoteControlPanelDemo()\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/SubCommentItem.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.gestures.animateScrollBy\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.bv.util.focusedBorder\nimport dev.aaa1115910.bv.util.isDpadDown\nimport dev.aaa1115910.bv.util.isKeyDown\nimport dev.aaa1115910.bv.util.isDpadRight\nimport kotlinx.coroutines.launch\n\n/**\n * 子评论项组件\n *\n * 子评论有焦点边框，但不响应点击\n *\n * @param comment 评论数据\n * @param modifier 修饰符\n * @param onLongClick 长按回调\n */\n@Composable\nfun SubCommentItem(\n    comment: Comment,\n    modifier: Modifier = Modifier,\n    onLongClick: () -> Unit = {}\n) {\n    // 子评论有焦点边框，但不响应点击\n    Surface(\n        modifier = modifier\n            .fillMaxWidth()\n            .focusedBorder(MaterialTheme.shapes.small),\n        onClick = { /* 空回调，不执行任何操作 */ },\n        onLongClick = onLongClick,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = Color.Transparent,\n            focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),\n            pressedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)\n        ),\n        scale = ClickableSurfaceDefaults.scale(\n            focusedScale = 1f,\n            pressedScale = 1f\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.small)\n    ) {\n        Row(\n            modifier = Modifier.padding(12.dp),\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            verticalAlignment = androidx.compose.ui.Alignment.Top\n        ) {\n            // 头像\n            AsyncImage(\n                model = comment.member.avatar,\n                modifier = Modifier\n                    .size(32.dp)\n                    .clip(CircleShape),\n                contentDescription = null,\n                contentScale = ContentScale.Crop\n            )\n\n            // 内容\n            Column(\n                modifier = Modifier.weight(1f),\n                verticalArrangement = Arrangement.spacedBy(4.dp)\n            ) {\n                // 用户名\n                Text(\n                    text = comment.member.name,\n                    style = MaterialTheme.typography.bodySmall,\n                    color = Color.White.copy(alpha = 0.7f)\n                )\n\n                // 评论内容（支持富文本表情）\n                CommentContent(\n                    content = comment.content,\n                    emotes = comment.emotes\n                )\n\n                // 评论图片\n                if (comment.pictures.isNotEmpty()) {\n                    CommentPictures(\n                        pictures = comment.pictures,\n                        modifier = Modifier.padding(top = 4.dp)\n                    )\n                }\n\n                // 底部信息\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(16.dp)\n                ) {\n                    Text(\n                        text = comment.timeDesc,\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White.copy(alpha = 0.5f)\n                    )\n                    Text(\n                        text = \"${comment.like} 赞\",\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White.copy(alpha = 0.5f)\n                    )\n                }\n            }\n        }\n    }\n}\n\n/**\n * 子评论根评论显示组件（只读，右键展开/收起，展开后下键滚动）\n *\n * @param comment 评论数据\n * @param onLongClick 长按回调\n */\n@Composable\nfun SubCommentRootItem(\n    comment: Comment,\n    onLongClick: () -> Unit = {}\n) {\n    var expanded by remember { mutableStateOf(false) }\n    var contentExceedsLimit by remember { mutableStateOf(false) }\n    val focusRequester = remember { FocusRequester() }\n    val scrollState = rememberScrollState()\n    val scope = rememberCoroutineScope()\n    val density = LocalDensity.current\n\n    Surface(\n        onClick = { /* 右键展开/收起 */ },\n        onLongClick = onLongClick,\n        modifier = Modifier\n            .fillMaxWidth()\n            .focusRequester(focusRequester)\n            .onPreviewKeyEvent { event ->\n                when {\n                    // 右键展开/收起（仅内容超出高度限制时）\n                    event.isKeyDown() && event.isDpadRight() && contentExceedsLimit -> {\n                        expanded = !expanded\n                        if (expanded) {\n                            scope.launch { scrollState.scrollTo(0) }\n                        }\n                        true\n                    }\n                    // 展开状态下，下键滚动内容，不允许焦点转移\n                    event.isKeyDown() && event.isDpadDown() && expanded -> {\n                        scope.launch {\n                            val scrollAmount = with(density) { 100.dp.toPx() }\n                            scrollState.animateScrollBy(scrollAmount)\n                        }\n                        true // 始终拦截事件，防止焦点转移\n                    }\n                    else -> false\n                }\n            },\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = Color.Transparent,\n            focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f),\n            pressedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f)\n        ),\n        scale = ClickableSurfaceDefaults.scale(\n            focusedScale = 1f,\n            pressedScale = 1f\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.small)\n    ) {\n        Column(\n            modifier = Modifier.padding(12.dp),\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Row(\n                modifier = Modifier\n                    .then(\n                        if (expanded) {\n                            Modifier.heightIn(max = 300.dp).verticalScroll(scrollState)\n                        } else {\n                            Modifier.heightIn(max = 150.dp)\n                        }\n                    )\n                    .onSizeChanged { size ->\n                        if (!expanded && !contentExceedsLimit) {\n                            val limitPx = with(density) { 150.dp.toPx() }\n                            contentExceedsLimit = size.height >= limitPx.toInt()\n                        }\n                    },\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n                verticalAlignment = androidx.compose.ui.Alignment.Top\n            ) {\n                AsyncImage(\n                    model = comment.member.avatar,\n                    modifier = Modifier\n                        .size(40.dp)\n                        .clip(CircleShape),\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop\n                )\n\n                Column(\n                    modifier = Modifier.weight(1f),\n                    verticalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    Text(\n                        text = comment.member.name,\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = Color.White.copy(alpha = 0.7f)\n                    )\n\n                    // 评论内容（支持富文本表情，最多3行）\n                    CommentContent(\n                        content = comment.content,\n                        emotes = comment.emotes,\n                        maxLines = if (expanded) Int.MAX_VALUE else 3,\n                        overflow = TextOverflow.Ellipsis\n                    )\n\n                    // 评论图片\n                    if (comment.pictures.isNotEmpty()) {\n                        CommentPictures(\n                            pictures = comment.pictures,\n                            modifier = Modifier.padding(top = 4.dp)\n                        )\n                    }\n\n                    Row(\n                        horizontalArrangement = Arrangement.spacedBy(16.dp)\n                    ) {\n                        Text(\n                            text = comment.timeDesc,\n                            style = MaterialTheme.typography.bodySmall,\n                            color = Color.White.copy(alpha = 0.5f)\n                        )\n                        Text(\n                            text = \"${comment.like} 赞\",\n                            style = MaterialTheme.typography.bodySmall,\n                            color = Color.White.copy(alpha = 0.5f)\n                        )\n                    }\n                }\n            }\n\n            // 展开/收起提示（仅内容超出高度限制时显示）\n            if (contentExceedsLimit) {\n                Text(\n                    text = if (expanded) \"右键收起 <<\" else \"右键展开 >>\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = Color.White,\n                    modifier = Modifier.padding(start = 48.dp)\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/SubCommentPanel.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.gestures.animateScrollBy\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material.Divider\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.SurfaceDefaults\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.reply.Comment\nimport dev.aaa1115910.biliapi.entity.reply.CommentReplyPage\nimport dev.aaa1115910.biliapi.repositories.CommentRepository\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.focusedBorder\nimport dev.aaa1115910.bv.util.isDpadDown\nimport dev.aaa1115910.bv.util.isKeyDown\nimport dev.aaa1115910.bv.util.onBackPressed\nimport dev.aaa1115910.bv.util.requestFocus\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.koin.compose.getKoin\n\n/**\n * 子评论浮窗组件\n *\n * @param show 是否显示浮窗\n * @param oid 视频 ID\n * @param rootId 根评论 ID\n * @param rootComment 根评论数据\n * @param onHide 关闭回调\n */\n@Composable\nfun SubCommentPanel(\n    show: Boolean,\n    oid: Long,\n    rootId: Long,\n    rootComment: Comment,\n    onHide: () -> Unit\n) {\n    val commentRepository: CommentRepository = getKoin().get()\n    val scope = rememberCoroutineScope()\n    val listState = rememberLazyListState()\n    val focusRequester = remember { FocusRequester() }\n    val density = LocalDensity.current\n\n    val replies = remember { mutableStateListOf<Comment>() }\n    var focusedCommentIndex by remember { mutableStateOf(0) }\n    var loading by remember { mutableStateOf(false) }\n    var currentPage by remember { mutableStateOf(CommentReplyPage()) }\n    var hasNext by remember { mutableStateOf(true) }\n    var error by remember { mutableStateOf<String?>(null) }\n\n    // 全屏图片查看器状态\n    var showImageViewer by remember { mutableStateOf(false) }\n    var imageViewerPictures by remember { mutableStateOf<List<dev.aaa1115910.biliapi.entity.Picture>>(emptyList()) }\n\n    // 加载子评论\n    val loadReplies: (Boolean) -> Unit = { reset ->\n        scope.launch {\n            if (loading) return@launch\n            loading = true\n            error = null\n\n            try {\n                val page = if (reset) CommentReplyPage() else currentPage\n                val data = commentRepository.getCommentReplies(\n                    rpid = rootId,\n                    type = 1L,\n                    commentId = oid,\n                    page = page,\n                    preferApiType = Prefs.apiType\n                )\n\n                if (reset) {\n                    replies.clear()\n                    replies.addAll(data.replies)\n                } else {\n                    replies.addAll(data.replies)\n                }\n\n                currentPage = data.nextPage\n                hasNext = data.hasNext\n            } catch (e: Exception) {\n                error = e.message ?: \"加载失败\"\n            } finally {\n                loading = false\n            }\n        }\n    }\n\n    // 显示时加载第一页\n    LaunchedEffect(show, rootId) {\n        if (show && replies.isEmpty()) {\n            loadReplies(true)\n        }\n    }\n\n    // 显示后请求焦点\n    LaunchedEffect(show, replies.isNotEmpty()) {\n        if (show && replies.isNotEmpty()) {\n            delay(300)\n            focusRequester.requestFocus(scope)\n        }\n    }\n\n    // 懒加载\n    val isAtBottom by remember {\n        derivedStateOf {\n            listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == replies.size - 1\n        }\n    }\n    LaunchedEffect(isAtBottom, hasNext, loading) {\n        if (isAtBottom && hasNext && !loading && replies.isNotEmpty()) {\n            loadReplies(false)\n        }\n    }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .clickable(onClick = onHide),\n        contentAlignment = Alignment.CenterEnd\n    ) {\n        AnimatedVisibility(\n            visible = show,\n            enter = expandHorizontally(expandFrom = Alignment.End),\n            exit = shrinkHorizontally(shrinkTowards = Alignment.End)\n        ) {\n            Surface(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .padding(horizontal = 16.dp, vertical = 16.dp)\n                    .widthIn(min = 320.dp, max = 420.dp)\n                    .fillMaxWidth(0.3f)\n                    .clickable(enabled = true, onClick = {})\n                    .onBackPressed { onHide() },\n                colors = SurfaceDefaults.colors(\n                    containerColor = Color.Black.copy(alpha = 0.85f)\n                ),\n                shape = MaterialTheme.shapes.large\n            ) {\n                Column(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .padding(16.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    // 根评论（只读显示，右键展开/收起）\n                    SubCommentRootItem(\n                        comment = rootComment,\n                        onLongClick = {\n                            if (rootComment.pictures.isNotEmpty()) {\n                                imageViewerPictures = rootComment.pictures\n                                showImageViewer = true\n                            }\n                        }\n                    )\n\n                    // 分隔线\n                    Divider(\n                        color = Color.White.copy(alpha = 0.2f),\n                        thickness = 1.dp\n                    )\n\n                    // 子评论列表\n                    if (error != null) {\n                        Text(\n                            text = error ?: \"加载失败\",\n                            color = Color.Red,\n                            modifier = Modifier.padding(16.dp)\n                        )\n                    } else if (replies.isEmpty() && !loading) {\n                        Text(\n                            text = \"暂无回复\",\n                            color = Color.White.copy(alpha = 0.5f),\n                            modifier = Modifier.padding(16.dp)\n                        )\n                    } else {\n                        LazyColumn(\n                            modifier = Modifier\n                                .weight(1f)\n                                .fillMaxWidth()\n                                .focusRequester(focusRequester)\n                                .onPreviewKeyEvent { event ->\n                                    when {\n                                        // 左键返回顶部\n                                        event.isKeyDown() && event.key == Key.DirectionLeft -> {\n                                            scope.launch {\n                                                listState.scrollToItem(0)\n                                                delay(100)\n                                                focusRequester.requestFocus(scope)\n                                            }\n                                            true\n                                        }\n                                        // 下键：逐步滚动，在列表末尾时阻止焦点移出\n                                        event.isKeyDown() && event.isDpadDown() -> {\n                                            val layoutInfo = listState.layoutInfo\n\n                                            // 已聚焦到最后一条回复时，阻止焦点移出列表\n                                            if (focusedCommentIndex >= replies.size - 1 && replies.isNotEmpty()) {\n                                                true\n                                            } else {\n                                                val currentItemInfo = layoutInfo.visibleItemsInfo\n                                                    .firstOrNull { it.index == focusedCommentIndex }\n\n                                                if (currentItemInfo != null) {\n                                                    val viewportEnd = layoutInfo.viewportEndOffset\n                                                    val itemBottom = currentItemInfo.offset + currentItemInfo.size\n\n                                                    // 如果评论底部不可见，逐步滚动\n                                                    if (itemBottom > viewportEnd) {\n                                                        scope.launch {\n                                                            // 每次滚动约 100dp\n                                                            val scrollAmount = with(density) { 100.dp.toPx() }\n                                                            listState.animateScrollBy(scrollAmount)\n                                                        }\n                                                        true // 拦截事件，不允许焦点转移\n                                                    } else {\n                                                        false // 评论已完全可见，允许焦点转移\n                                                    }\n                                                } else {\n                                                    false\n                                                }\n                                            }\n                                        }\n                                        else -> false\n                                    }\n                                },\n                            state = listState,\n                            verticalArrangement = Arrangement.spacedBy(8.dp)\n                        ) {\n                            itemsIndexed(\n                                items = replies,\n                                key = { index, it -> \"$index-reply-${it.rpid}\" }\n                            ) { index, reply ->\n                                SubCommentItem(\n                                    comment = reply,\n                                    modifier = Modifier\n                                        .fillMaxWidth()\n                                        .onFocusChanged { focusState ->\n                                            if (focusState.hasFocus) {\n                                                focusedCommentIndex = index\n                                            }\n                                        },\n                                    onLongClick = {\n                                        if (reply.pictures.isNotEmpty()) {\n                                            imageViewerPictures = reply.pictures\n                                            showImageViewer = true\n                                        }\n                                    }\n                                )\n                            }\n\n                            if (loading) {\n                                item {\n                                    Row(\n                                        modifier = Modifier.fillMaxWidth().padding(16.dp),\n                                        horizontalArrangement = Arrangement.Center\n                                    ) {\n                                        LoadingTip()\n                                    }\n                                }\n                            }\n\n                            if (!hasNext && replies.isNotEmpty()) {\n                                item {\n                                    Text(\n                                        modifier = Modifier.fillMaxWidth().padding(16.dp),\n                                        text = \"没有更多了\",\n                                        style = MaterialTheme.typography.bodySmall,\n                                        color = Color.White.copy(alpha = 0.5f)\n                                    )\n                                }\n                            }\n                        }\n                    }\n\n                    // 底部提示\n                    Spacer(modifier = Modifier.padding(bottom = 8.dp))\n                }\n            }\n        }\n    }\n\n    // 全屏图片查看器\n    if (showImageViewer && imageViewerPictures.isNotEmpty()) {\n        FullscreenImageViewer(\n            pictures = imageViewerPictures,\n            onDismiss = {\n                showImageViewer = false\n                imageViewerPictures = emptyList()\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/TopNav.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport android.content.Context\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusProperties\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.isSpecified\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Tab\nimport androidx.tv.material3.TabRow\nimport androidx.tv.material3.TabRowScope\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.entity.NavSwitchMode\nimport dev.aaa1115910.bv.util.getDisplayName\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.isKeyDown\nimport kotlinx.coroutines.delay\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun TopNav(\n    modifier: Modifier = Modifier,\n    paddingTop: Dp = 12.dp,\n    items: List<TopNavItem>,\n    useSmallSize: Boolean = false,\n    initialSelectedItem: TopNavItem? = null,\n    navSwitchMode: NavSwitchMode = NavSwitchMode.Auto,\n    tabFocusRequester: FocusRequester? = null,\n    itemFocusRequesterProvider: ((Int, TopNavItem) -> FocusRequester?)? = null,\n    onSelectedChanged: (TopNavItem) -> Unit = {},\n    onClick: (TopNavItem) -> Unit = {},\n    onLeftKeyEvent: () -> Unit = {}\n) {\n    val sizeScale = if (useSmallSize) 0.8f else 1f\n    val topPadding = paddingTop * sizeScale\n    val horizontalPadding = 12.dp * (if (useSmallSize) 1 / sizeScale else 1f)\n    val bottomPadding = 8.dp * sizeScale\n    val separatorWidth = 12.dp * sizeScale\n\n    if (items.isEmpty()) {\n        Row(\n            modifier = modifier\n                .fillMaxWidth()\n                .padding(\n                    top = topPadding,\n                    bottom = bottomPadding,\n                    start = horizontalPadding,\n                    end = horizontalPadding\n                ),\n            horizontalArrangement = Arrangement.Center\n        ) {}\n        return\n    }\n\n    val defaultFocusRequester = tabFocusRequester ?: remember { FocusRequester() }\n\n    var selectedNav by remember(initialSelectedItem) {\n        mutableStateOf(initialSelectedItem ?: items.first())\n    }\n\n    var selectedTabIndex by remember(initialSelectedItem) {\n        mutableIntStateOf(\n            if (initialSelectedItem != null) {\n                val index = items.indexOf(initialSelectedItem)\n                if (index >= 0) index else 0\n            } else 0\n        )\n    }\n\n    // 在确认键切换模式下，focusedTabIndex 跟踪当前聚焦的 tab（视觉高亮）\n    var focusedTabIndex by remember(initialSelectedItem) {\n        mutableIntStateOf(\n            if (initialSelectedItem != null) {\n                val index = items.indexOf(initialSelectedItem)\n                if (index >= 0) index else 0\n            } else 0\n        )\n    }\n\n    var tabMoved by remember { mutableStateOf(true) }\n\n    val currentTabFocusRequester = run {\n        val focusIndex = (if (navSwitchMode == NavSwitchMode.Confirm) focusedTabIndex else selectedTabIndex)\n            .coerceIn(items.indices)\n        itemFocusRequesterProvider?.invoke(focusIndex, items[focusIndex]) ?: defaultFocusRequester\n    }\n\n    LaunchedEffect(items, initialSelectedItem) {\n        if (items.isEmpty()) return@LaunchedEffect\n\n        val nextSelectedItem = initialSelectedItem?.takeIf { it in items } ?: items.first()\n        selectedNav = nextSelectedItem\n        val idx = items.indexOf(nextSelectedItem).takeIf { it >= 0 } ?: 0\n        selectedTabIndex = idx\n        focusedTabIndex = idx\n        tabMoved = true\n    }\n\n    LaunchedEffect(selectedNav) {\n        if (selectedNav !in items) return@LaunchedEffect\n        delay(200)\n        onSelectedChanged(selectedNav)\n        // 别急着向下移动焦点，动画还没结束\n        delay(400)\n        tabMoved = true\n    }\n\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(\n                top = topPadding,\n                bottom = bottomPadding,\n                start = horizontalPadding,\n                end = horizontalPadding\n            )\n            .onFocusChanged {\n                if (!it.hasFocus) {\n                    focusedTabIndex = selectedTabIndex\n                }\n            },\n        horizontalArrangement = Arrangement.Center\n    ) {\n        TabRow(\n            modifier = Modifier\n                .then(\n                    if (navSwitchMode == NavSwitchMode.Auto) {\n                        Modifier.focusRestorer(currentTabFocusRequester)\n                    } else {\n                        Modifier.focusProperties {\n                            enter = { currentTabFocusRequester }\n                        }\n                    }\n                )\n                .onPreviewKeyEvent {\n                    if (it.isKeyDown()) {\n                        val currentFocusIndex = if (navSwitchMode == NavSwitchMode.Confirm) focusedTabIndex else selectedTabIndex\n                        if (it.key == Key.DirectionLeft && currentFocusIndex == 0) {\n                            onLeftKeyEvent()\n                            return@onPreviewKeyEvent true\n                        }\n                        if (it.key == Key.DirectionDown) {\n                            return@onPreviewKeyEvent !tabMoved\n                        }\n                    }\n                    false\n                },\n            selectedTabIndex = selectedTabIndex,\n            indicator = { _, _ -> },\n            separator = { Spacer(modifier = Modifier.width(separatorWidth)) },\n        ) {\n            items.forEachIndexed { index, tab ->\n                val itemFocusRequester = itemFocusRequesterProvider?.invoke(index, tab)\n                val itemFocusModifier = itemFocusRequester?.let { Modifier.focusRequester(it) } ?: Modifier\n                val useSharedFocusRequester = itemFocusRequester == null &&\n                        if (navSwitchMode == NavSwitchMode.Confirm) index == focusedTabIndex else index == selectedTabIndex\n                NavItemTab(\n                    modifier = Modifier\n                        .then(itemFocusModifier)\n                        .ifElse(\n                            useSharedFocusRequester,\n                            Modifier.focusRequester(defaultFocusRequester)\n                        ),\n                    topNavItem = tab,\n                    useSmallSize = useSmallSize,\n                    selected = index == selectedTabIndex,\n                    focused = navSwitchMode == NavSwitchMode.Confirm && index == focusedTabIndex && index != selectedTabIndex,\n                    onFocus = {\n                        if (navSwitchMode == NavSwitchMode.Auto) {\n                            // 自动切换模式：聚焦即切换\n                            val isSameTab = tab == selectedNav\n                            selectedNav = tab\n                            selectedTabIndex = index\n                            if (!isSameTab) {\n                                tabMoved = false\n                            }\n                        } else {\n                            // 确认键切换模式：聚焦只更新视觉状态\n                            focusedTabIndex = index\n                        }\n                    },\n                    onClick = {\n                        if (navSwitchMode == NavSwitchMode.Confirm) {\n                            // 确认键切换模式：按确认键才切换\n                            val isSameTab = tab == selectedNav\n                            selectedNav = tab\n                            selectedTabIndex = index\n                            focusedTabIndex = index\n                            if (!isSameTab) {\n                                tabMoved = false\n                            }\n                        }\n                        onClick(tab)\n                    }\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun TabRowScope.NavItemTab(\n    modifier: Modifier = Modifier,\n    topNavItem: TopNavItem,\n    useSmallSize: Boolean = false,\n    selected: Boolean,\n    focused: Boolean = false,\n    onClick: () -> Unit,\n    onFocus: () -> Unit\n) {\n    val context = LocalContext.current\n    var isFocused by remember { mutableStateOf(false) }\n    val sizeScale = if (useSmallSize) 0.85f else 1f\n    val tabHeight = 32.dp * sizeScale\n    val tabHorizontalPadding = 16.dp * sizeScale\n    val tabCornerRadius = 50.dp * sizeScale\n    val baseTextStyle = MaterialTheme.typography.bodyLarge\n    val textStyle = if (useSmallSize) {\n        baseTextStyle.copy(\n            fontSize = if (baseTextStyle.fontSize.isSpecified) baseTextStyle.fontSize * sizeScale else TextUnit.Unspecified,\n            lineHeight = if (baseTextStyle.lineHeight.isSpecified) baseTextStyle.lineHeight * sizeScale else TextUnit.Unspecified,\n            letterSpacing = if (baseTextStyle.letterSpacing.isSpecified) baseTextStyle.letterSpacing * sizeScale else TextUnit.Unspecified\n        )\n    } else {\n        baseTextStyle\n    }\n\n    Tab(\n        modifier = modifier.onFocusChanged { isFocused = it.hasFocus },\n        selected = selected,\n        onFocus = onFocus,\n        onClick = onClick\n    ) {\n        val actualFocused = isFocused || focused\n        Text(\n            modifier = Modifier\n                .height(tabHeight)\n                .ifElse(\n                    !actualFocused && selected,\n                    Modifier.background(\n                        color = MaterialTheme.colorScheme.inverseSurface,\n                        shape = RoundedCornerShape(tabCornerRadius)\n                    )\n                )\n                .ifElse(\n                    actualFocused && !selected,\n                    Modifier.background(\n                        color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.3f),\n                        shape = RoundedCornerShape(tabCornerRadius)\n                    )\n                )\n                .ifElse(\n                    actualFocused && selected,\n                    Modifier.background(\n                        color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.75f),\n                        shape = RoundedCornerShape(tabCornerRadius)\n                    )\n                )\n                .wrapContentHeight(Alignment.CenterVertically)\n                .padding(horizontal = tabHorizontalPadding),\n            text = topNavItem.getDisplayName(context),\n            style = textStyle,\n            color = if (!actualFocused && selected) MaterialTheme.colorScheme.surface \n                    else if (actualFocused && !selected) MaterialTheme.colorScheme.inverseSurface\n                    else if (actualFocused && selected) MaterialTheme.colorScheme.surface\n                    else MaterialTheme.colorScheme.inverseSurface\n        )\n    }\n}\n\ninterface TopNavItem {\n    fun getDisplayName(context: Context = BVApp.context): String\n}\n\nenum class HomeTopNavItem(private val displayName: String) : TopNavItem {\n    Recommend(\"推荐\"),\n    Popular(\"热门\"),\n    Dynamics(\"动态\"),\n    History(\"历史\"),\n    Favorite(\"收藏\"),\n    FollowingSeason(\"追番\"),\n    ToView(\"稍后再看\");\n\n    override fun getDisplayName(context: Context): String {\n        return displayName\n    }\n}\n\nenum class UgcTopNavItem(private val ugcType: UgcTypeV2) : TopNavItem {\n    Douga(UgcTypeV2.Douga),\n    Game(UgcTypeV2.Game),\n    Kichiku(UgcTypeV2.Kichiku),\n    Music(UgcTypeV2.Music),\n    Dance(UgcTypeV2.Dance),\n    Cinephile(UgcTypeV2.Cinephile),\n    Ent(UgcTypeV2.Ent),\n    Knowledge(UgcTypeV2.Knowledge),\n    Tech(UgcTypeV2.Tech),\n    Information(UgcTypeV2.Information),\n    Food(UgcTypeV2.Food),\n    ShortPlay(UgcTypeV2.Shortplay),\n    Car(UgcTypeV2.Car),\n    Fashion(UgcTypeV2.Fashion),\n    Sports(UgcTypeV2.Sports),\n    Animal(UgcTypeV2.Animal),\n    Vlog(UgcTypeV2.Vlog),\n    Painting(UgcTypeV2.Painting),\n    Ai(UgcTypeV2.Ai),\n    Home(UgcTypeV2.Home),\n    Outdoors(UgcTypeV2.Outdoors),\n    Gym(UgcTypeV2.Gym),\n    Handmake(UgcTypeV2.Handmake),\n    Travel(UgcTypeV2.Travel),\n    Rural(UgcTypeV2.Rural),\n    Parenting(UgcTypeV2.Parenting),\n    Health(UgcTypeV2.Health),\n    Emotion(UgcTypeV2.Emotion),\n    LifeJoy(UgcTypeV2.LifeJoy),\n    LifeExperience(UgcTypeV2.LifeExperience),\n    Mysticism(UgcTypeV2.Mysticism);\n\n    override fun getDisplayName(context: Context): String {\n        return ugcType.getDisplayName(context)\n    }\n}\n\nenum class PgcTopNavItem(private val pgcType: PgcType) : TopNavItem {\n    Anime(PgcType.Anime),\n    GuoChuang(PgcType.GuoChuang),\n    Movie(PgcType.Movie),\n    Documentary(PgcType.Documentary),\n    Tv(PgcType.Tv),\n    Variety(PgcType.Variety);\n\n    override fun getDisplayName(context: Context): String {\n        return pgcType.getDisplayName(context)\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/TvAlertDialog.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.AlertDialogDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.ProvideTextStyle\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun TvAlertDialog(\n    onDismissRequest: () -> Unit,\n    confirmButton: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n    dismissButton: @Composable (() -> Unit)? = null,\n    icon: @Composable (() -> Unit)? = null,\n    title: @Composable (() -> Unit)? = null,\n    text: @Composable (() -> Unit)? = null,\n    shape: Shape = AlertDialogDefaults.shape,\n    containerColor: Color = AlertDialogDefaults.containerColor,\n    iconContentColor: Color = AlertDialogDefaults.iconContentColor,\n    titleContentColor: Color = AlertDialogDefaults.titleContentColor,\n    textContentColor: Color = AlertDialogDefaults.textContentColor,\n    tonalElevation: Dp = AlertDialogDefaults.TonalElevation,\n    properties: DialogProperties = DialogProperties()\n) {\n    AlertDialog(\n        onDismissRequest = onDismissRequest,\n        confirmButton = confirmButton,\n        modifier = modifier,\n        dismissButton = dismissButton,\n        icon = icon,\n        title = (@Composable {\n            ProvideTextStyle(\n                value = MaterialTheme.typography.headlineSmall\n            ) {\n                title?.invoke()\n            }\n        }).takeIf { title != null },\n        text = (@Composable {\n            ProvideTextStyle(\n                value = MaterialTheme.typography.bodyMedium\n            ) {\n                text?.invoke()\n            }\n        }).takeIf { title != null },\n        shape = shape,\n        containerColor = containerColor,\n        iconContentColor = iconContentColor,\n        titleContentColor = titleContentColor,\n        textContentColor = textContentColor,\n        tonalElevation = tonalElevation,\n        properties = properties\n    )\n}\n\n@Preview\n@Composable\nprivate fun DialogPreview() {\n    BVTheme {\n        TvAlertDialog(\n            title = {\n                Text(text = \"Dialog Title\")\n            },\n            text = {\n                Column {\n                    Text(text = \"This is a sample dialog text. It can be used to display information or ask for user input.\")\n                    Text(\n                        text = \"This is a sample dialog text. It can be used to display information or ask for user input.\",\n                        style = MaterialTheme.typography.bodyMedium\n                    )\n                }\n            },\n            onDismissRequest = {},\n            confirmButton = {\n                OutlinedButton(onClick = {}) {\n                    Text(text = \"Confirm\")\n                }\n            },\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/UpIcon.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun UpIcon(\n    modifier: Modifier = Modifier,\n    color: Color = MaterialTheme.colorScheme.onSurface\n) {\n    Icon(\n        modifier = modifier,\n        painter = painterResource(id = R.drawable.ic_up),\n        contentDescription = null,\n        tint = color\n    )\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun UpIconPreview() {\n    BVTheme {\n        Row(\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            UpIcon()\n            Text(text = \"bishi\")\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/UserPanel.kt",
    "content": "package dev.aaa1115910.bv.tv.component\n\nimport android.content.res.Configuration\nimport android.view.KeyEvent\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.CrueltyFree\nimport androidx.compose.material.icons.rounded.FavoriteBorder\nimport androidx.compose.material.icons.rounded.History\nimport androidx.compose.material.icons.rounded.Schedule\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.requestFocus\n\nprivate val lineHeight = 80.dp\n\n@Composable\nfun UserPanel(\n    modifier: Modifier = Modifier,\n    username: String,\n    face: String,\n    onHide: () -> Unit,\n    onGoMy: () -> Unit,\n    onGoHistory: () -> Unit,\n    onGoFavorite: () -> Unit,\n    onGoFollowing: () -> Unit,\n    onGoLater: () -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus(scope)\n    }\n\n    Box(\n        modifier = modifier\n            .onPreviewKeyEvent {\n                when (it.nativeKeyEvent.keyCode) {\n                    KeyEvent.KEYCODE_BACK -> {\n                        if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) onHide()\n                        return@onPreviewKeyEvent true\n                    }\n                }\n                false\n            }\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(24.dp)\n        ) {\n            UserPanelMyItem(\n                modifier = Modifier\n                    .width(300.dp)\n                    .focusRequester(focusRequester)\n                    .onPreviewKeyEvent {\n                        when (it.nativeKeyEvent.keyCode) {\n                            KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_DPAD_LEFT -> {\n                                return@onPreviewKeyEvent true\n                            }\n                        }\n                        false\n                    },\n                username = username,\n                face = face,\n                onClick = {\n                    onGoMy()\n                    onHide()\n                }\n            )\n\n            val buttonWidth = 120.dp\n            Row {\n                UserPanelSmallItem(\n                    modifier = Modifier\n                        .width(buttonWidth)\n                        .onPreviewKeyEvent {\n                            when (it.nativeKeyEvent.keyCode) {\n                                KeyEvent.KEYCODE_DPAD_LEFT -> {\n                                    return@onPreviewKeyEvent true\n                                }\n                            }\n                            false\n                        },\n                    title = \"历史记录\",\n                    icon = Icons.Rounded.History,\n                    onClick = {\n                        onGoHistory()\n                        onHide()\n                    }\n                )\n                UserPanelSmallItem(\n                    modifier = Modifier\n                        .width(buttonWidth),\n                    title = \"私人藏品\",\n                    icon = Icons.Rounded.FavoriteBorder,\n                    onClick = {\n                        onGoFavorite()\n                        onHide()\n                    }\n                )\n                UserPanelSmallItem(\n                    modifier = Modifier\n                        .width(buttonWidth),\n                    title = \"我追的番\",\n                    icon = Icons.Rounded.CrueltyFree,\n                    onClick = {\n                        onGoFollowing()\n                        onHide()\n                    }\n                )\n                UserPanelSmallItem(\n                    modifier = Modifier\n                        .width(buttonWidth),\n                    title = \"现在不看\",\n                    icon = Icons.Rounded.Schedule,\n                    onClick = {\n                        onGoLater()\n                        onHide()\n                    }\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun UserPanelMyItem(\n    modifier: Modifier = Modifier,\n    username: String,\n    face: String,\n    onClick: () -> Unit\n) {\n    Surface(\n        modifier = modifier\n            .padding(4.dp)\n            .fillMaxWidth()\n            .height(lineHeight),\n        onClick = onClick,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surfaceVariant,\n            focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n            pressedContainerColor = MaterialTheme.colorScheme.inverseSurface\n        )\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(12.dp),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Column(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .padding(end = 40.dp),\n                verticalArrangement = Arrangement.SpaceAround\n            ) {\n                Text(text = username)\n                Text(text = \"\")\n            }\n            AsyncImage(\n                modifier = Modifier\n                    .size(40.dp)\n                    .clip(CircleShape),\n                model = face,\n                contentDescription = null,\n                contentScale = ContentScale.FillBounds\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun UserPanelSmallItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    icon: ImageVector,\n    onClick: () -> Unit\n) {\n    Surface(\n        modifier = modifier\n            .padding(4.dp)\n            .height(lineHeight),\n        onClick = onClick,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surfaceVariant,\n            focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n            pressedContainerColor = MaterialTheme.colorScheme.inverseSurface\n        )\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize()\n        ) {\n            Icon(\n                modifier = Modifier\n                    .padding(12.dp)\n                    .align(Alignment.TopStart),\n                imageVector = icon,\n                contentDescription = null\n            )\n            Text(\n                modifier = Modifier\n                    .padding(12.dp)\n                    .align(Alignment.BottomStart),\n                text = title,\n                style = MaterialTheme.typography.bodyLarge\n            )\n        }\n\n    }\n}\n\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun UserPanelPreview() {\n    BVTheme {\n        UserPanel(\n            username = \"\",\n            face = \"\",\n            onHide = {},\n            onGoMy = {},\n            onGoHistory = {},\n            onGoFollowing = {},\n            onGoFavorite = {},\n            onGoLater = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/buttons/CoinButton.kt",
    "content": "package dev.aaa1115910.bv.tv.component.buttons\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Paid\nimport androidx.compose.material.icons.outlined.Paid\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.ButtonBorder\nimport androidx.tv.material3.ButtonColors\nimport androidx.tv.material3.ButtonDefaults\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.LocalContentColor\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun CoinButton(\n    modifier: Modifier = Modifier,\n    isCoin: Boolean,\n    onAddCoin: () -> Unit = {},\n    contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 6.dp), // 减小内边距\n    colors: ButtonColors = ButtonDefaults.colors(),\n    border: ButtonBorder = ButtonDefaults.border()\n) {\n    Button(\n        modifier = modifier,\n        contentPadding = contentPadding,\n        colors = colors,\n        border = border,\n        shape = ButtonDefaults.shape(shape = RoundedCornerShape(8.dp)), // 设置为小圆角\n        onClick = {onAddCoin()}\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.spacedBy(8.dp), // 减小间距\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Icon(\n                modifier = Modifier\n                    .size(16.dp),\n                imageVector = if (isCoin) Icons.Rounded.Paid else Icons.Outlined.Paid,\n                contentDescription = null,\n                tint = if (isCoin) Color(0xfffb7299) else LocalContentColor.current\n            )\n            Text(\n                text = stringResource(R.string.coin_button_text)\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nfun CoinButtonEnablePreview() {\n    BVTheme {\n        CoinButton(\n            isCoin = true\n        )\n    }\n}\n\n@Preview\n@Composable\nfun CoinButtonDisablePreview() {\n    BVTheme {\n        CoinButton(\n            isCoin = false\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/buttons/FavoriteButton.kt",
    "content": "package dev.aaa1115910.bv.tv.component.buttons\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Done\nimport androidx.compose.material.icons.rounded.Favorite\nimport androidx.compose.material.icons.rounded.FavoriteBorder\nimport androidx.compose.material3.AlertDialogDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.ButtonBorder\nimport androidx.tv.material3.ButtonColors\nimport androidx.tv.material3.ButtonDefaults\nimport androidx.tv.material3.ExperimentalTvMaterial3Api\nimport androidx.tv.material3.FilterChip\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.LocalContentColor\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.FavoriteFolderMetadata\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.manager.VideoUserActionManager\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.swapList\nimport kotlinx.coroutines.delay\n\n@Composable\nfun FavoriteButton(\n    modifier: Modifier = Modifier,\n    isFavorite: Boolean,\n    favoriteFolderIds: List<Long> = emptyList(),\n    onAddToDefaultFavoriteFolder: () -> Unit,\n    onUpdateFavoriteFolders: (List<Long>) -> Unit,\n    contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 6.dp), // 减小内边距\n    colors: ButtonColors = ButtonDefaults.colors(),\n    border: ButtonBorder = ButtonDefaults.border(),\n    onDialogVisibilityChanged: (Boolean) -> Unit = {},\n    dialogContainerColor: Color = AlertDialogDefaults.containerColor\n) {\n    val userFavoriteFolders by VideoUserActionManager.getFavoriteFoldersFlow().collectAsState()\n    var showFavoriteDialog by remember { mutableStateOf(false) }\n\n    LaunchedEffect(showFavoriteDialog) {\n        onDialogVisibilityChanged(showFavoriteDialog)\n    }\n\n    Button(\n        modifier = modifier,\n        contentPadding = contentPadding,\n        colors = colors,\n        border = border,\n        shape = ButtonDefaults.shape(shape = RoundedCornerShape(8.dp)), // 设置为小圆角\n        onClick = {\n            if (showFavoriteDialog) return@Button\n            if (isFavorite) {\n                // 有收藏状态，显示收藏夹选择对话框\n                showFavoriteDialog = true\n            } else {\n                // 无收藏状态\n                if (userFavoriteFolders.size > 1) {\n                    // 有多个收藏夹，显示收藏夹选择对话框\n                    showFavoriteDialog = true\n                } else {\n                    // 否则使用默认收藏夹\n                    onAddToDefaultFavoriteFolder()\n                }\n            }\n        }\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.spacedBy(4.dp), // 减小间距\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Icon(\n                modifier = Modifier\n                    .size(16.dp),\n                imageVector = if (isFavorite) Icons.Rounded.Favorite else Icons.Rounded.FavoriteBorder,\n                contentDescription = null,\n                tint = if (isFavorite) Color(0xfffb7299) else LocalContentColor.current\n            )\n            Text(\n                text = stringResource(R.string.favorite_button_text)\n            )\n        }\n    }\n\n    FavoriteDialog(\n        show = showFavoriteDialog,\n        onHideDialog = { showFavoriteDialog = false },\n        userFavoriteFolders = userFavoriteFolders,\n        favoriteFolderIds = favoriteFolderIds,\n        onUpdateFavoriteFolders = onUpdateFavoriteFolders,\n        dialogContainerColor = dialogContainerColor\n    )\n}\n\n@OptIn(ExperimentalTvMaterial3Api::class)\n@Composable\nprivate fun FavoriteDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    userFavoriteFolders: List<FavoriteFolderMetadata> = emptyList(),\n    favoriteFolderIds: List<Long> = emptyList(),\n    onUpdateFavoriteFolders: (List<Long>) -> Unit,\n    dialogContainerColor: Color = AlertDialogDefaults.containerColor\n) {\n    val selectedFavoriteFolderIds = remember { mutableStateListOf<Long>() }\n    val defaultFocusRequester = remember { FocusRequester() }\n    var lastInteractionTime by remember { mutableStateOf(System.currentTimeMillis()) }\n    fun touch() { lastInteractionTime = System.currentTimeMillis() }\n\n    LaunchedEffect(show) {\n        if (show) {\n            selectedFavoriteFolderIds.swapList(favoriteFolderIds)\n            defaultFocusRequester.requestFocus()\n            // 打开时更新交互时间\n            touch()\n        }\n    }\n    // 10 秒无操作自动关闭\n    LaunchedEffect(lastInteractionTime, show) {\n        if (show) {\n            val base = lastInteractionTime\n            delay(10000)\n            if (base == lastInteractionTime) onHideDialog()\n        }\n    }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            containerColor = dialogContainerColor,\n            onDismissRequest = onHideDialog,\n            confirmButton = {},\n            title = { Text(text = stringResource(R.string.favorite_dialog_title)) },\n            text = {\n                FlowRow(\n                    modifier = Modifier\n                        .width(400.dp)\n                        .verticalScroll(rememberScrollState())\n                        .padding(vertical = 8.dp),\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    userFavoriteFolders.forEachIndexed { index, userFavoriteFolder ->\n                        val selected = selectedFavoriteFolderIds.contains(userFavoriteFolder.id)\n                        var hasFocus by remember { mutableStateOf(false) }\n\n                        val itemModifier =\n                            if (index == 0) Modifier.focusRequester(defaultFocusRequester)\n                            else Modifier\n\n                        FilterChip(\n                            modifier = itemModifier.onFocusChanged {\n                                hasFocus = it.hasFocus\n                                if (it.hasFocus) touch()\n                            },\n                            selected = selected,\n                            onClick = {\n                                if (selectedFavoriteFolderIds.contains(userFavoriteFolder.id)) {\n                                    selectedFavoriteFolderIds.remove(userFavoriteFolder.id)\n                                } else {\n                                    selectedFavoriteFolderIds.add(userFavoriteFolder.id)\n                                }\n                                onUpdateFavoriteFolders(selectedFavoriteFolderIds)\n                                // 点击交互更新最后交互时间\n                                touch()\n                            },\n                            leadingIcon = {\n                                Row {\n                                    if(selected) {\n                                        Icon(\n                                            modifier = Modifier.size(20.dp),\n                                            imageVector = Icons.Rounded.Done,\n                                            contentDescription = null\n                                        )\n                                    }\n                                }\n                            }\n                        ) {\n                            Text(text = userFavoriteFolder.title)\n                        }\n                    }\n                }\n            }\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun FavoriteButtonEnablePreview() {\n    BVTheme {\n        FavoriteButton(\n            isFavorite = true,\n            onAddToDefaultFavoriteFolder = {},\n            onUpdateFavoriteFolders = {}\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun FavoriteButtonDisablePreview() {\n    BVTheme {\n        FavoriteButton(\n            isFavorite = false,\n            onAddToDefaultFavoriteFolder = {},\n            onUpdateFavoriteFolders = {}\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun FavoriteDialogPreview() {\n    val userFavoriteFolders = listOf(\n        FavoriteFolderMetadata(0, 0, 0, \"收藏夹1\", null, false, 0),\n        FavoriteFolderMetadata(1, 1, 0, \"收藏夹2\", null, false, 0),\n        FavoriteFolderMetadata(2, 2, 0, \"收藏夹3\", null, false, 0),\n        FavoriteFolderMetadata(3, 3, 0, \"收藏夹4\", null, false, 0),\n        FavoriteFolderMetadata(4, 4, 0, \"收藏夹5\", null, false, 0),\n        FavoriteFolderMetadata(5, 5, 0, \"收藏夹6\", null, false, 0),\n        FavoriteFolderMetadata(6, 6, 0, \"收藏夹7\", null, false, 0),\n        FavoriteFolderMetadata(7, 7, 0, \"收藏夹8\", null, false, 0),\n        FavoriteFolderMetadata(8, 8, 0, \"收藏夹9\", null, false, 0),\n        FavoriteFolderMetadata(9, 9, 0, \"收藏夹10\", null, false, 0),\n    )\n    BVTheme {\n        FavoriteDialog(\n            show = true,\n            onHideDialog = {},\n            userFavoriteFolders = userFavoriteFolders,\n            onUpdateFavoriteFolders = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/buttons/LikeButton.kt",
    "content": "package dev.aaa1115910.bv.tv.component.buttons\n\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.ThumbUp\nimport androidx.compose.material.icons.rounded.ThumbUp\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.ButtonBorder\nimport androidx.tv.material3.ButtonColors\nimport androidx.tv.material3.ButtonDefaults\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.LocalContentColor\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun LikeButton(\n    modifier: Modifier = Modifier,\n    isLike: Boolean,\n    onToggleLike: () -> Unit = {},\n    contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 6.dp), // 减小内边距\n    colors: ButtonColors = ButtonDefaults.colors(),\n    border: ButtonBorder = ButtonDefaults.border()\n) {\n    Button(\n        modifier = modifier,\n        onClick = {onToggleLike()},\n        contentPadding = contentPadding,\n        shape = ButtonDefaults.shape(shape = RoundedCornerShape(8.dp)), // 设置为小圆角\n        colors = colors,\n        border = border,\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.spacedBy(4.dp), // 减小间距\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Icon(\n                modifier = Modifier\n                    .size(16.dp),\n                imageVector = if (isLike) Icons.Rounded.ThumbUp else Icons.Outlined.ThumbUp,\n                contentDescription = null,\n                tint = if (isLike) Color(0xfffb7299) else LocalContentColor.current\n            )\n            Text(\n                text = stringResource(R.string.like_button_text)\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nfun LikeButtonEnablePreview() {\n    BVTheme {\n        LikeButton(\n            isLike = true\n        )\n    }\n}\n\n@Preview\n@Composable\nfun LikeButtonDisablePreview() {\n    BVTheme {\n        LikeButton(\n            isLike = false\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/buttons/SeasonInfoButtons.kt",
    "content": "package dev.aaa1115910.bv.tv.component.buttons\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Favorite\nimport androidx.compose.material.icons.rounded.FavoriteBorder\nimport androidx.compose.material.icons.rounded.PlayArrow\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.IconButton\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.OutlinedButtonDefaults\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.focusedBorder\n\n@Composable\nfun SeasonInfoButtons(\n    modifier: Modifier = Modifier,\n    lastPlayedIndex: Int,\n    lastPlayedTitle: String = \"\",\n    following: Boolean,\n    isPublished: Boolean,\n    publishDate: String,\n    onPlay: () -> Unit,\n    onClickFollow: (follow: Boolean) -> Unit,\n    onShowComment: () -> Unit = {},\n    commentButtonFocusRequester: FocusRequester = remember { FocusRequester() },\n    playButtonFocusRequester: FocusRequester = remember { FocusRequester() }\n) {\n    Row(\n        modifier = modifier\n            .padding(4.dp),\n        horizontalArrangement = Arrangement.spacedBy(16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        if (isPublished) {\n            Button(\n                onClick = onPlay,\n                modifier = Modifier.focusRequester(playButtonFocusRequester)\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Icon(\n                        modifier = Modifier.size(20.dp),\n                        imageVector = Icons.Rounded.PlayArrow,\n                        contentDescription = null\n                    )\n                    Text(text = if (lastPlayedIndex == -1) \"开始播放\" else lastPlayedTitle)\n                }\n\n            }\n        } else {\n            Button(onClick = {}) {\n                Text(text = publishDate)\n            }\n        }\n        // 追番按钮\n        FollowSeasonButton(\n            following = following,\n            onClick = onClickFollow\n        )\n        // 评论按钮\n        Column (\n            modifier = Modifier\n                .focusRequester(commentButtonFocusRequester)\n                .clip(MaterialTheme.shapes.small)\n                .background(Color.White.copy(alpha = 0.2f))\n                .focusedBorder(MaterialTheme.shapes.small)\n                .padding(horizontal = 8.dp, vertical = 4.dp)\n                .clickable { onShowComment() },\n            verticalArrangement = Arrangement.Center,\n        ) {\n            Text(\n                text = \"评论>>\",\n                color = MaterialTheme.colorScheme.onSurface\n            )\n        }\n    }\n}\n\n@Composable\nfun FollowSeasonButton(\n    modifier: Modifier = Modifier,\n    following: Boolean,\n    onClick: (follow: Boolean) -> Unit\n) {\n    IconButton(\n        modifier = modifier,\n        onClick = { onClick(!following) },\n        colors = OutlinedButtonDefaults.colors(),\n        border = OutlinedButtonDefaults.border()\n    ) {\n        Box(\n            modifier = Modifier.size(20.dp)\n        ) {\n            if (following) {\n                Icon(\n                    imageVector = Icons.Rounded.Favorite,\n                    contentDescription = null,\n                    tint = Color(0xfffb7299)\n                )\n            } else {\n                Icon(\n                    imageVector = Icons.Rounded.FavoriteBorder,\n                    contentDescription = null\n                )\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nfun SeasonInfoButtonsPreview() {\n    BVTheme {\n        SeasonInfoButtons(\n            lastPlayedIndex = 3,\n            lastPlayedTitle = \"拯救灵依计划\",\n            following = false,\n            isPublished = true,\n            publishDate = \"2021-10-01\",\n            onPlay = {},\n            onClickFollow = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/buttons/ToViewButton.kt",
    "content": "package dev.aaa1115910.bv.tv.component.buttons\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.AccessTime\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.ButtonBorder\nimport androidx.tv.material3.ButtonColors\nimport androidx.tv.material3.ButtonDefaults\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun ToViewButton(\n    modifier: Modifier = Modifier,\n    onAddToView: () -> Unit = {},\n    contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 6.dp),\n    colors: ButtonColors = ButtonDefaults.colors(),\n    border: ButtonBorder = ButtonDefaults.border()\n) {\n    Button(\n        modifier = modifier,\n        onClick = { onAddToView() },\n        contentPadding = contentPadding,\n        shape = ButtonDefaults.shape(shape = RoundedCornerShape(8.dp)),\n        colors = colors,\n        border = border,\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.spacedBy(4.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Icon(\n                modifier = Modifier.size(16.dp),\n                imageVector = Icons.Outlined.AccessTime,\n                contentDescription = null\n            )\n            Text(\n                text = stringResource(R.string.toview_button_text)\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nfun ToViewButtonPreview() {\n    BVTheme {\n        ToViewButton()\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/live/LiveRoomCard.kt",
    "content": "package dev.aaa1115910.bv.tv.component.live\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport java.util.Locale\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.live.LiveRoomItem\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.resizedImageUrl\n\n@Composable\nfun LiveRoomCard(\n    modifier: Modifier = Modifier,\n    data: LiveRoomItem,\n    onClick: () -> Unit = {},\n    onFocus: () -> Unit = {}\n) {\n    var hasFocus by remember { mutableStateOf(false) }\n\n    Surface(\n        modifier = modifier\n            .fillMaxWidth()\n            .onFocusChanged {\n                hasFocus = it.isFocused\n                if (hasFocus) onFocus()\n            }\n            .ifElse(\n                hasFocus,\n                Modifier.border(\n                    width = 2.dp,\n                    color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f),\n                    shape = MaterialTheme.shapes.medium\n                )\n            ),\n        onClick = onClick,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = Color.Transparent,\n            focusedContainerColor = if (hasFocus) MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f) else Color.Transparent,\n            pressedContainerColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f)\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        scale = ClickableSurfaceDefaults.scale(scale = 1f, focusedScale = 1f)\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(3.dp)\n        ) {\n            // 封面\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .aspectRatio(16f / 9f)\n                    .clip(MaterialTheme.shapes.medium)\n            ) {\n                AsyncImage(\n                    model = data.cover.resizedImageUrl(ImageSize.LargeCover),\n                    contentDescription = null,\n                    modifier = Modifier.fillMaxSize(),\n                    contentScale = ContentScale.Crop\n                )\n\n                // 底部渐变遮罩\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(60.dp)\n                        .align(Alignment.BottomCenter)\n                        .background(\n                            Brush.verticalGradient(\n                                colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.7f))\n                            )\n                        )\n                )\n\n                // 直播中标识（仅开播时显示）\n                if (data.liveStatus == 1) {\n                    Box(\n                        modifier = Modifier\n                            .padding(8.dp)\n                            .background(\n                                color = Color(0xFFFF6699).copy(alpha = 0.6f),\n                                shape = RoundedCornerShape(4.dp)\n                            )\n                            .padding(horizontal = 8.dp, vertical = 4.dp)\n                            .align(Alignment.TopStart)\n                    ) {\n                        Text(\n                            text = \"直播中\",\n                            color = Color.White,\n                            fontSize = 12.sp\n                        )\n                    }\n                }\n\n                // 在线人数\n                Row(\n                    modifier = Modifier\n                        .padding(8.dp)\n                        .align(Alignment.BottomStart),\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Icon(\n                        modifier = Modifier.size(16.dp),\n                        painter = painterResource(id = R.drawable.ic_play_count),\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                    Spacer(modifier = Modifier.width(4.dp))\n                    Text(\n                        text = formatViewCount(\n                            data.watchedShow?.num ?: (data.online / 10)\n                        ),\n                        color = Color.White,\n                        fontSize = 13.sp\n                    )\n                }\n            }\n\n            Spacer(modifier = Modifier.height(8.dp))\n\n            // 直播间标题\n            Text(\n                text = data.title,\n                maxLines = 2,\n                minLines = 2,\n                overflow = TextOverflow.Ellipsis,\n                style = MaterialTheme.typography.bodyMedium,\n                modifier = Modifier.padding(horizontal = 4.dp)\n            )\n\n            Spacer(modifier = Modifier.height(4.dp))\n\n            // 主播信息\n            Row(\n                modifier = Modifier.padding(horizontal = 4.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                // 主播头像\n                AsyncImage(\n                    model = data.face.resizedImageUrl(ImageSize.Icon),\n                    contentDescription = null,\n                    modifier = Modifier\n                        .size(20.dp)\n                        .clip(CircleShape),\n                    contentScale = ContentScale.Crop\n                )\n                Spacer(modifier = Modifier.width(4.dp))\n                Text(\n                    text = data.uname,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                )\n            }\n\n            Spacer(modifier = Modifier.height(4.dp))\n        }\n    }\n}\n\n/**\n * 格式化观看人数\n */\nprivate fun formatViewCount(count: Int): String {\n    return when {\n        count >= 100_000_000 -> String.format(Locale.US, \"%.1f亿\", count / 100_000_000.0)\n        count >= 10_000 -> String.format(Locale.US, \"%.1f万\", count / 10_000.0)\n        else -> count.toString()\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/pgc/IndexFilter.kt",
    "content": "package dev.aaa1115910.bv.tv.component.pgc\n\nimport android.content.res.Configuration\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material3.Icon\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.tooling.preview.PreviewParameter\nimport androidx.compose.ui.tooling.preview.PreviewParameterProvider\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.tv.material3.ExperimentalTvMaterial3Api\nimport androidx.tv.material3.FilterChip\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.entity.pgc.index.PGC_INDEX_ORDER_FIELD\nimport dev.aaa1115910.biliapi.entity.pgc.index.PgcIndexOption\nimport dev.aaa1115910.biliapi.entity.pgc.index.PgcIndexSection\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.getDisplayName\n\n@Composable\nfun IndexFilter(\n    modifier: Modifier = Modifier,\n    type: PgcType,\n    show: Boolean,\n    onDismissRequest: () -> Unit,\n    sections: List<PgcIndexSection>,\n    selectedFilters: Map<String, PgcIndexOption>,\n    onFilterChange: (PgcIndexOption) -> Unit,\n    onResetFilters: () -> Unit\n) {\n    val context = LocalContext.current\n\n    IndexFilterContent(\n        modifier = modifier,\n        title = stringResource(R.string.pgc_index_filter_title_prefix) + type.getDisplayName(context),\n        show = show,\n        onDismissRequest = onDismissRequest,\n        sections = sections,\n        selectedFilters = selectedFilters,\n        onFilterChange = onFilterChange,\n        onResetFilters = onResetFilters\n    )\n}\n\n@Composable\nprivate fun IndexFilterContent(\n    modifier: Modifier = Modifier,\n    title: String,\n    show: Boolean,\n    onDismissRequest: () -> Unit,\n    sections: List<PgcIndexSection>,\n    selectedFilters: Map<String, PgcIndexOption>,\n    onFilterChange: (PgcIndexOption) -> Unit,\n    onResetFilters: () -> Unit\n) {\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier\n                .fillMaxWidth(0.8f),\n            onDismissRequest = onDismissRequest,\n            confirmButton = {\n                if (sections.isNotEmpty()) {\n                    OutlinedButton(onClick = onResetFilters) {\n                        Text(text = stringResource(R.string.filter_dialog_reset))\n                    }\n                }\n            },\n            title = {\n                Text(text = title)\n            },\n            text = {\n                LazyColumn(\n                    modifier = Modifier.heightIn(max = 300.dp),\n                    verticalArrangement = Arrangement.spacedBy(12.dp)\n                ) {\n                    items(\n                        items = sections.filter { it.options.isNotEmpty() },\n                        key = { section -> section.field }\n                    ) { section ->\n                        IndexFilterChipRow(\n                            title = section.title,\n                            options = section.options,\n                            selectedFilter = selectedFilters[section.field],\n                            onFilterChange = onFilterChange\n                        )\n                    }\n                }\n            },\n            properties = DialogProperties(usePlatformDefaultWidth = false)\n        )\n    }\n}\n\n@OptIn(ExperimentalTvMaterial3Api::class)\n@Composable\nprivate fun IndexFilterChip(\n    modifier: Modifier = Modifier,\n    selected: Boolean,\n    onClick: () -> Unit,\n    label: String\n) {\n    FilterChip(\n        modifier = modifier,\n        selected = selected,\n        onClick = onClick\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.spacedBy(4.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            AnimatedVisibility(visible = selected) {\n                Icon(\n                    modifier = Modifier.size(20.dp),\n                    imageVector = Icons.Default.Check,\n                    contentDescription = null\n                )\n            }\n            Text(text = label)\n        }\n    }\n}\n\n@Composable\nprivate fun IndexFilterChipRow(\n    modifier: Modifier = Modifier,\n    title: String,\n    options: List<PgcIndexOption>,\n    selectedFilter: PgcIndexOption?,\n    onFilterChange: (PgcIndexOption) -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n\n    Row(\n        horizontalArrangement = Arrangement.spacedBy(8.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Text(\n            text = title,\n            style = MaterialTheme.typography.labelLarge\n        )\n        LazyRow(\n            modifier = modifier\n                .focusRestorer(focusRequester),\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)\n        ) {\n            items(\n                items = options,\n                key = { option -> \"${option.field}:${option.keyword}:${option.sort.orEmpty()}\" }\n            ) { option ->\n                IndexFilterChip(\n                    modifier = if (selectedFilter == option) Modifier.focusRequester(focusRequester) else Modifier,\n                    selected = selectedFilter == option,\n                    onClick = { onFilterChange(option) },\n                    label = option.name\n                )\n            }\n        }\n    }\n}\n\nprivate class PgcTypeProvider : PreviewParameterProvider<PgcType> {\n    override val values = PgcType.entries.asSequence()\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun IndexFilterPreview(\n    @PreviewParameter(PgcTypeProvider::class) pgcType: PgcType\n) {\n    val sections = remember {\n        listOf(\n            PgcIndexSection(\n                field = PGC_INDEX_ORDER_FIELD,\n                title = \"排序\",\n                options = listOf(\n                    PgcIndexOption(PGC_INDEX_ORDER_FIELD, \"8\", \"综合排序\", sort = \"0\"),\n                    PgcIndexOption(PGC_INDEX_ORDER_FIELD, \"3\", \"最多追番\", sort = \"0\"),\n                    PgcIndexOption(PGC_INDEX_ORDER_FIELD, \"0\", \"最近更新\", sort = \"0\")\n                )\n            ),\n            PgcIndexSection(\n                field = \"area\",\n                title = \"地区\",\n                options = listOf(\n                    PgcIndexOption(\"area\", \"-1\", \"全部地区\"),\n                    PgcIndexOption(\"area\", \"1,6,7\", \"国产\"),\n                    PgcIndexOption(\"area\", \"2\", \"日本\"),\n                    PgcIndexOption(\"area\", \"3\", \"美国\")\n                )\n            ),\n            PgcIndexSection(\n                field = \"season_status\",\n                title = \"付费类型\",\n                options = listOf(\n                    PgcIndexOption(\"season_status\", \"-1\", \"全部付费\"),\n                    PgcIndexOption(\"season_status\", \"1\", \"免费\"),\n                    PgcIndexOption(\"season_status\", \"2,6\", \"付费\"),\n                    PgcIndexOption(\"season_status\", \"4,6\", \"大会员\")\n                )\n            )\n        )\n    }\n    val selectedFilters = remember {\n        mutableStateMapOf<String, PgcIndexOption>().apply {\n            sections.forEach { section ->\n                section.options.firstOrNull()?.let { option ->\n                    put(section.field, option)\n                }\n            }\n        }\n    }\n\n    BVTheme {\n        Surface(\n            modifier = Modifier.fillMaxSize()\n        ) {\n            IndexFilter(\n                type = pgcType,\n                show = true,\n                onDismissRequest = { },\n                sections = sections,\n                selectedFilters = selectedFilters,\n                onFilterChange = { option -> selectedFilters[option.field] = option },\n                onResetFilters = {\n                    selectedFilters.clear()\n                    sections.forEach { section ->\n                        section.options.firstOrNull()?.let { option ->\n                            selectedFilters[section.field] = option\n                        }\n                    }\n                }\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/search/SearchKeyword.kt",
    "content": "package dev.aaa1115910.bv.tv.component.search\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.DenseListItem\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImagePainter\nimport coil.compose.rememberAsyncImagePainter\nimport coil.request.ImageRequest\nimport coil.size.Size\n\n@Composable\nfun SearchKeyword(\n    modifier: Modifier = Modifier,\n    keyword: String,\n    leadingIcon: String,\n    trailingIcon: @Composable() (() -> Unit)? = null,\n    onClick: () -> Unit\n) {\n    val context = LocalContext.current\n    // 使用全局 ImageLoader，无需手动创建\n    val painter = rememberAsyncImagePainter(\n        ImageRequest.Builder(context)\n            .data(data = leadingIcon)\n            .size(Size.ORIGINAL)\n            .build(),\n        contentScale = ContentScale.FillHeight\n    )\n\n    if (leadingIcon != \"\" && painter.state is AsyncImagePainter.State.Success) {\n        DenseListItem(\n            modifier = modifier,\n            selected = false,\n            onClick = onClick,\n            headlineContent = {\n                Text(\n                    text = keyword,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n            },\n            leadingContent = {\n                Image(\n                    modifier = Modifier.height(16.dp),\n                    painter = painter,\n                    contentDescription = null,\n                )\n            },\n            trailingContent = trailingIcon\n        )\n    } else {\n        DenseListItem(\n            modifier = modifier,\n            selected = false,\n            onClick = onClick,\n            headlineContent = {\n                Text(\n                    text = keyword,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n            },\n            trailingContent = trailingIcon\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/search/SoftKeyboard.kt",
    "content": "package dev.aaa1115910.bv.tv.component.search\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Checkbox\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun SoftKeyboard(\n    modifier: Modifier = Modifier,\n    firstButtonFocusRequester: FocusRequester,\n    showSearchWithProxy: Boolean,\n    enableSearchWithProxy: Boolean,\n    onClick: (String) -> Unit,\n    onClear: () -> Unit,\n    onDelete: () -> Unit,\n    onSearch: () -> Unit,\n    onEnableSearchWithProxyChange: (Boolean) -> Unit\n) {\n    val keys = listOf(\n        listOf(\"A\", \"B\", \"C\", \"D\", \"E\", \"F\"),\n        listOf(\"G\", \"H\", \"I\", \"J\", \"K\", \"L\"),\n        listOf(\"M\", \"N\", \"O\", \"P\", \"Q\", \"R\"),\n        listOf(\"S\", \"T\", \"U\", \"V\", \"W\", \"X\"),\n        listOf(\"Y\", \"Z\", \"1\", \"2\", \"3\", \"4\"),\n        listOf(\"5\", \"6\", \"7\", \"8\", \"9\", \"0\")\n    )\n\n    Column(\n        modifier = modifier.width(258.dp),\n        verticalArrangement = Arrangement.spacedBy(6.dp)\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.spacedBy(6.dp)\n        ) {\n            SoftKeyboardButton(\n                modifier = Modifier.weight(1f),\n                key = stringResource(R.string.search_input_soft_keybord_clear),\n                onClick = onClear\n            )\n            SoftKeyboardButton(\n                modifier = Modifier.weight(1f),\n                key = stringResource(R.string.search_input_soft_keybord_delete),\n                onClick = onDelete\n            )\n            SoftKeyboardButton(\n                modifier = Modifier.weight(1f),\n                key = stringResource(R.string.search_input_soft_keybord_search),\n                onClick = onSearch\n            )\n        }\n        if (showSearchWithProxy) {\n            Surface(\n                modifier = Modifier,\n                onClick = { onEnableSearchWithProxyChange(!enableSearchWithProxy) },\n                colors = ClickableSurfaceDefaults.colors(\n                    focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n                    pressedContainerColor = MaterialTheme.colorScheme.inverseSurface\n                )\n            ) {\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(vertical = 4.dp),\n                    horizontalArrangement = Arrangement.Center,\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Checkbox(\n                        checked = enableSearchWithProxy,\n                        onCheckedChange = { onEnableSearchWithProxyChange(it) },\n                    )\n                    Text(text = \"通过代理搜索\")\n                }\n            }\n        }\n        keys.forEachIndexed { rowIndex, rowKeys ->\n            Row(\n                horizontalArrangement = Arrangement.spacedBy(6.dp)\n            ) {\n                rowKeys.forEachIndexed { index, key ->\n                    val keyModifier = if (rowIndex == 0 && index == 0) {\n                        Modifier.focusRequester(firstButtonFocusRequester)\n                    } else {\n                        Modifier\n                    }\n                    SoftKeyboardKey(\n                        modifier = keyModifier,\n                        key = key,\n                        onClick = { onClick(key) }\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun SoftKeyboardKey(\n    modifier: Modifier = Modifier,\n    key: String,\n    onClick: () -> Unit\n) {\n    Surface(\n        modifier = modifier,\n        onClick = onClick\n    ) {\n        Box(\n            modifier = Modifier.size(38.dp),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(\n                text = key,\n                style = MaterialTheme.typography.titleMedium\n            )\n        }\n    }\n}\n\n@Composable\nfun SoftKeyboardButton(\n    modifier: Modifier = Modifier,\n    key: String,\n    onClick: () -> Unit\n) {\n    Surface(\n        modifier = modifier.height(38.dp),\n        onClick = onClick\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(\n                text = key,\n                style = MaterialTheme.typography.titleMedium\n            )\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun SoftKeyboardKeyPreview() {\n    BVTheme {\n        SoftKeyboardKey(\n            key = \"X\",\n            onClick = {}\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun SoftKeyboardPreview() {\n    val firstButtonFocusRequester = remember { FocusRequester() }\n    BVTheme {\n        SoftKeyboard(\n            firstButtonFocusRequester = firstButtonFocusRequester,\n            showSearchWithProxy = true,\n            enableSearchWithProxy = true,\n            onClick = {},\n            onClear = {},\n            onDelete = {},\n            onSearch = {},\n            onEnableSearchWithProxyChange = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/settings/SettingListItem.kt",
    "content": "package dev.aaa1115910.bv.tv.component.settings\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.ListItem\nimport androidx.tv.material3.RadioButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\n\n@Composable\nfun SettingListItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    supportText: String,\n    defaultHasFocus: Boolean = false,\n    onClick: () -> Unit,\n    valueText: String? = null\n) {\n    var hasFocus by remember { mutableStateOf(defaultHasFocus) }\n\n    ListItem(\n        modifier = modifier.onFocusChanged { hasFocus = it.hasFocus },\n        headlineContent = { Text(text = title) },\n        supportingContent = { Text(text = supportText) },\n        trailingContent = { if (valueText?.isNotEmpty() == true) Text(modifier = Modifier.padding(start = 4.dp), text = valueText) },\n        onClick = onClick,\n        selected = false\n    )\n}\n\n@Composable\nfun <T> SettingListItemWithDialog(\n    modifier: Modifier = Modifier,\n    title: String,\n    supportText: String,\n    options: List<T>,\n    getDisplayName: (T, Context) -> String,\n    value: T,\n    onValueChange: (T) -> Unit,\n    defaultHasFocus: Boolean = false\n) {\n    val context = LocalContext.current\n    var showDialog by remember { mutableStateOf(false) }\n\n    SettingListItem(\n        modifier = modifier,\n        title = title,\n        supportText = supportText,\n        defaultHasFocus = defaultHasFocus,\n        valueText = getDisplayName(value, context),\n        onClick = { showDialog = true }\n    )\n\n    SelectionDialog(\n        show = showDialog,\n        title = title,\n        onHideDialog = { showDialog = false },\n        options = options,\n        getDisplayName = getDisplayName,\n        value = value,\n        onChange = onValueChange\n    )\n}\n\n\n@SuppressLint(\"ConfigurationScreenWidthHeight\")\n@Composable\nfun <T> SelectionDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    title: String = \"\",\n    options: List<T>,\n    getDisplayName: (T, Context) -> String,\n    value: T,\n    onChange: (T) -> Unit,\n    onHideDialog: () -> Unit\n) {\n    if (show) {\n        val context = LocalContext.current\n        val configuration = LocalConfiguration.current\n        val maxHeight = (configuration.screenHeightDp * 0.5).dp\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = { onHideDialog() },\n            title = { if (title.isNotEmpty()) Text(text = title) },\n            text = {\n                Column(\n                    modifier = Modifier\n                        .heightIn(max = maxHeight)\n                        .verticalScroll(rememberScrollState())\n                ) {\n                    options.forEach {\n                        ListItem(\n                            selected = value == it,\n                            onClick = { onChange(it) },\n                            headlineContent = {\n                                Text(text = getDisplayName(it, context))\n                            },\n                            trailingContent = {\n                                RadioButton(\n                                    selected = value == it,\n                                    onClick = null\n                                )\n                            }\n                        )\n                    }\n                }\n            },\n            confirmButton = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/settings/SettingNumberListItem.kt",
    "content": "package dev.aaa1115910.bv.tv.component.settings\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.ListItem\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.clickable\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Remove\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.rounded.ArrowDropUp\nimport androidx.compose.material.icons.rounded.ArrowDropDown\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.IconButton\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.text.style.TextAlign\nimport kotlin.math.round\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalContext\nimport dev.aaa1115910.bv.util.requestFocus\nimport kotlin.math.max\nimport kotlin.math.min\n\n@Composable\nfun SettingNumberListItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    supportText: String,\n    value: Double,\n    minValue: Double = 0.0,\n    maxValue: Double = 100.0,\n    isInteger: Boolean = true,\n    step: Double = 1.0,\n    defaultHasFocus: Boolean = false,\n    onValueChange: (Double) -> Unit\n) {\n    var hasFocus by remember { mutableStateOf(defaultHasFocus) }\n    var currentValue by remember { mutableStateOf(value) }\n    var showDialog by remember { mutableStateOf(false) }\n\n    ListItem(\n        modifier = modifier.onFocusChanged { hasFocus = it.hasFocus },\n        headlineContent = { Text(text = title) },\n        supportingContent = { Text(text = supportText) },\n        trailingContent = {\n            Text(\n                modifier = Modifier\n                    .widthIn(48.dp, 96.dp),\n                text = if (isInteger) currentValue.toInt().toString() else String.format(\n                    \"%.2f\",\n                    currentValue\n                ),\n                textAlign = TextAlign.Center,\n                style = MaterialTheme.typography.bodyMedium,\n                color = if (hasFocus) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.onSurface\n            )\n        },\n        onClick = {\n            showDialog = true\n        },\n        selected = hasFocus\n    )\n\n    NumberDialog(\n        show = showDialog,\n        title = title,\n        initValue = currentValue,\n        minValue = minValue,\n        maxValue = maxValue,\n        step = step,\n        isInteger = isInteger,\n        onHideDialog = { showDialog = false },\n        onChange = { newValue ->\n            currentValue = newValue.toDouble()\n            onValueChange(currentValue)\n        }\n    )\n}\n\n@Composable\nprivate fun NumberDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    title: String? = null,\n    onHideDialog: () -> Unit,\n    onChange: (Float) -> Unit,\n    initValue: Double = 0.0,\n    minValue: Double = 0.0,\n    maxValue: Double = 100.0,\n    step: Double = 1.0,\n    isInteger: Boolean = true,\n) {\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n    var currentValue by remember { mutableStateOf(initValue) }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus(scope)\n    }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = { onHideDialog() },\n            title = { Text(text = title ?: \"\") },\n            text = {\n                Column(\n                    modifier = Modifier\n                        .focusRequester(focusRequester)\n                        .focusable()\n                        .fillMaxWidth()\n                        .onPreviewKeyEvent {\n                            if (it.key == Key.DirectionUp || it.key == Key.DirectionDown) {\n                                if (it.type == KeyEventType.KeyDown) {\n                                    val newValue = if (it.key == Key.DirectionUp) \n                                        (currentValue + step).coerceAtMost(maxValue)\n                                        else (currentValue - step).coerceAtLeast(minValue)\n                                    currentValue = if (isInteger) round(newValue) else newValue\n                                    onChange(currentValue.toFloat())\n                                }\n                                true\n                            } else if (listOf(Key.Enter, Key.DirectionCenter).contains(it.key) && it.type == KeyEventType.KeyDown) {\n                                onHideDialog()\n                                true\n                            } else {\n                                false\n                            }\n                        },\n                    horizontalAlignment = Alignment.CenterHorizontally\n                ) {\n                    Icon(imageVector = Icons.Rounded.ArrowDropUp, contentDescription = null)\n                    Text(\n                        text = if (isInteger) currentValue.toInt().toString() else String.format(\"%.2f\", currentValue),\n                        style = MaterialTheme.typography.headlineMedium\n                    )\n                    Icon(imageVector = Icons.Rounded.ArrowDropDown, contentDescription = null)\n                }\n            },\n            confirmButton = {}\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SettingNumberListItemIntegerPreview() {\n    BVTheme {\n        SettingNumberListItem(\n            title = \"Integer Setting\",\n            supportText = \"This is an integer value setting\",\n            value = 50.0,\n            minValue = 0.0,\n            maxValue = 100.0,\n            isInteger = true,\n            defaultHasFocus = true,\n            onValueChange = {}\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SettingNumberListItemDecimalPreview() {\n    BVTheme {\n        SettingNumberListItem(\n            title = \"Decimal Setting\",\n            supportText = \"This is a decimal value setting\",\n            value = 2.25,\n            minValue = 0.0,\n            maxValue = 10.0,\n            isInteger = false,\n            step = 0.25,\n            defaultHasFocus = true,\n            onValueChange = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/settings/SettingSwitchListItem.kt",
    "content": "package dev.aaa1115910.bv.tv.component.settings\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.ListItem\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Switch\nimport androidx.tv.material3.SwitchDefaults\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun SettingSwitchListItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    supportText: String,\n    checked: Boolean,\n    defaultHasFocus: Boolean = false,\n    onCheckedChange: (Boolean) -> Unit\n) {\n    var hasFocus by remember { mutableStateOf(defaultHasFocus) }\n    var switchChecked by remember { mutableStateOf(checked) }\n\n    ListItem(\n        modifier = modifier.onFocusChanged { hasFocus = it.hasFocus },\n        headlineContent = { Text(text = title) },\n        supportingContent = { Text(text = supportText) },\n        trailingContent = {\n            Box(\n                modifier = Modifier\n                    .border(2.dp, MaterialTheme.colorScheme.surface, CircleShape)\n            ) {\n                Switch(\n                    modifier = Modifier\n                        .focusable(false)\n                        .padding(2.dp),\n                    checked = switchChecked,\n                    onCheckedChange = null,\n                    colors = SwitchDefaults.colors(\n                        checkedThumbColor = MaterialTheme.colorScheme.inverseSurface,\n                        checkedTrackColor = MaterialTheme.colorScheme.surfaceVariant,\n                        uncheckedThumbColor = MaterialTheme.colorScheme.onSurface,\n                        uncheckedTrackColor = MaterialTheme.colorScheme.surface\n                    )\n                )\n            }\n        },\n        onClick = {\n            switchChecked = !switchChecked\n            onCheckedChange(switchChecked)\n        },\n        selected = hasFocus\n    )\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SettingSwitchListItemFocusedAndEnabledPreview() {\n    BVTheme {\n        SettingSwitchListItem(\n            title = \"This is a title\",\n            supportText = \"This is a support text\",\n            checked = true,\n            defaultHasFocus = true,\n            onCheckedChange = {}\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SettingSwitchListItemFocusedAndDisabledPreview() {\n    BVTheme {\n        SettingSwitchListItem(\n            title = \"This is a title\",\n            supportText = \"This is a support text\",\n            checked = false,\n            defaultHasFocus = true,\n            onCheckedChange = {}\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SettingSwitchListItemNotFocusedAndEnabledPreview() {\n    BVTheme {\n        SettingSwitchListItem(\n            title = \"This is a title\",\n            supportText = \"This is a support text\",\n            checked = true,\n            defaultHasFocus = false,\n            onCheckedChange = {}\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SettingSwitchListItemNotFocusedAndDisabledPreview() {\n    BVTheme {\n        SettingSwitchListItem(\n            title = \"This is a title\",\n            supportText = \"This is a support text\",\n            checked = false,\n            defaultHasFocus = false,\n            onCheckedChange = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/settings/SettingsMenuSelectItem.kt",
    "content": "package dev.aaa1115910.bv.tv.component.settings\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.focusable\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.tooling.preview.PreviewParameter\nimport androidx.compose.ui.tooling.preview.PreviewParameterProvider\nimport androidx.tv.material3.ListItem\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.RadioButton\nimport androidx.tv.material3.RadioButtonDefaults\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun SettingsMenuSelectItem(\n    modifier: Modifier = Modifier,\n    text: String,\n    selected: Boolean,\n    defaultHasFocus: Boolean = false,\n    onClick: () -> Unit\n) {\n    var hasFocus by remember { mutableStateOf(defaultHasFocus) }\n\n    ListItem(\n        modifier = modifier.onFocusChanged { hasFocus = it.hasFocus },\n        headlineContent = { Text(text = text) },\n        trailingContent = {\n            RadioButton(\n                modifier = Modifier.focusable(false),\n                selected = selected,\n                onClick = { },\n                colors = RadioButtonDefaults.colors(\n                    selectedColor = if (hasFocus) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant,\n                    unselectedColor = if (hasFocus) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant\n                )\n            )\n        },\n        onClick = onClick,\n        selected = selected\n    )\n}\n\nprivate class SettingsMenuSelectItemPreviewParameterProvider :\n    PreviewParameterProvider<SettingsMenuSelectItemData> {\n    override val values = sequenceOf(\n        SettingsMenuSelectItemData(text = \"This is a text\", selected = false, onFocused = false),\n        SettingsMenuSelectItemData(text = \"This is a text\", selected = false, onFocused = true),\n        SettingsMenuSelectItemData(text = \"This is a text\", selected = true, onFocused = false),\n        SettingsMenuSelectItemData(text = \"This is a text\", selected = true, onFocused = true),\n    )\n}\n\nprivate data class SettingsMenuSelectItemData(\n    val text: String,\n    val selected: Boolean,\n    val onFocused: Boolean\n)\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun SettingsMenuSelectItemPreview(\n    @PreviewParameter(SettingsMenuSelectItemPreviewParameterProvider::class) data: SettingsMenuSelectItemData\n) {\n    BVTheme {\n        SettingsMenuSelectItem(\n            text = data.text,\n            selected = data.selected,\n            defaultHasFocus = data.onFocused,\n            onClick = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/settings/UpdateDialog.kt",
    "content": "package dev.aaa1115910.bv.tv.component.settings\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.component.settings.UpdateDialog\n\n@Composable\nfun UpdateDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit\n) {\n    UpdateDialog(\n        modifier = modifier,\n        show = show,\n        onHideDialog = onHideDialog,\n        text = { text ->\n            Text(text = text)\n        },\n        button = { enabled, onClick, content ->\n            Button(\n                enabled = enabled,\n                onClick = onClick,\n                content = content\n            )\n        },\n        outlinedButton = { enabled, onClick, content ->\n            OutlinedButton(\n                enabled = enabled,\n                onClick = onClick,\n                content = content\n            )\n        }\n    )\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/videocard/LargeVideoCard.kt",
    "content": "package dev.aaa1115910.bv.tv.component.videocard\n\nimport android.content.res.Configuration\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Card\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalView\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.SurfaceDefaults\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.tv.component.UpIcon\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.focusedBorder\n\n@Composable\nfun LargeVideoCard(\n    modifier: Modifier = Modifier,\n    data: VideoCardData,\n    onClick: () -> Unit = {},\n    onFocus: () -> Unit = {}\n) {\n    val view = LocalView.current\n\n    var hasFocus by remember { mutableStateOf(false) }\n    val scale by animateFloatAsState(\n        targetValue = if (hasFocus) 1f else 0.95f,\n        label = \"large video card scale\"\n    )\n\n    val height = 160.dp\n    val reasonColor = Color.Red\n\n    LaunchedEffect(hasFocus) {\n        if (hasFocus) onFocus()\n    }\n\n    Card(\n        modifier = modifier\n            .fillMaxWidth()\n            .scale(scale)\n            .onFocusChanged { hasFocus = it.isFocused }\n            .focusedBorder(MaterialTheme.shapes.medium)\n            .clickable { onClick() },\n        shape = MaterialTheme.shapes.medium\n    ) {\n        Row(\n            modifier = Modifier\n                .height(height)\n        ) {\n            Box {\n                if (!view.isInEditMode) {\n                    AsyncImage(\n                        modifier = Modifier\n                            .fillMaxHeight()\n                            .aspectRatio(1.6f)\n                            .clip(MaterialTheme.shapes.medium),\n                        model = data.cover,\n                        contentDescription = null,\n                        contentScale = ContentScale.FillBounds\n                    )\n                } else {\n                    Surface(\n                        modifier = Modifier\n                            .fillMaxHeight()\n                            .aspectRatio(1.6f),\n                        shape = MaterialTheme.shapes.medium,\n                        colors = SurfaceDefaults.colors(\n                            containerColor = Color.White\n                        )\n                    ) {}\n                }\n                Surface(\n                    modifier = Modifier\n                        .align(Alignment.BottomEnd)\n                        .padding(8.dp),\n                    colors = SurfaceDefaults.colors(\n                        containerColor = Color.Black.copy(alpha = 0.5f)\n                    ),\n                    shape = RoundedCornerShape(6.dp)\n                ) {\n                    Text(\n                        modifier = Modifier.padding(4.dp),\n                        text = data.timeString,\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White\n                    )\n                }\n            }\n            Column(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .padding(8.dp),\n                verticalArrangement = Arrangement.SpaceBetween\n            ) {\n                Text(\n                    text = data.title,\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis,\n                    style = MaterialTheme.typography.titleLarge\n                )\n                Box(\n                    modifier = Modifier\n                        .padding(start = 4.dp)\n                        .border(\n                            width = (1.5).dp,\n                            color = if (data.reason.isNotEmpty()) reasonColor else Color.Transparent,\n                            shape = RoundedCornerShape(6.dp)\n                        )\n                ) {\n                    Text(\n                        modifier = Modifier.padding(6.dp, 2.dp),\n                        text = data.reason,\n                        style = MaterialTheme.typography.bodySmall,\n                        color = reasonColor,\n                        fontWeight = FontWeight.Bold\n                    )\n                }\n\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    UpIcon()\n                    Text(text = data.upName)\n                }\n\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start)\n                ) {\n                    Text(text = \"P${data.playString}\")\n                    Text(text = \"D${data.danmakuString}\")\n                }\n            }\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun LargeVideoCardPreview() {\n    val data = VideoCardData(\n        avid = 0,\n        title = \"震惊！太震惊了！真的是太震惊了！我的天呐！真TMD震惊！\",\n        cover = \"http://i2.hdslb.com/bfs/archive/af17fc07b8f735e822563cc45b7b5607a491dfff.jpg\",\n        reason = \"本周必看\",\n        upName = \"bishi\",\n        play = 2333,\n        danmaku = 666,\n        time = 2333 * 1000\n    )\n    BVTheme {\n        Surface {\n            LargeVideoCard(\n                data = data\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/videocard/SeasonCard.kt",
    "content": "package dev.aaa1115910.bv.tv.component.videocard\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Border\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.entity.carddata.SeasonCardData\nimport dev.aaa1115910.bv.ui.theme.BVTheme\n\n@Composable\nfun SeasonCard(\n    modifier: Modifier = Modifier,\n    data: SeasonCardData,\n    coverHeight: Dp? = null,\n    onClick: () -> Unit = {},\n    onLongClick: () -> Unit = {},\n    onFocus: () -> Unit = {}\n) {\n    val localDensity = LocalDensity.current\n    var coverRealWidth by remember { mutableStateOf(0.dp) }\n\n    Surface(\n        modifier = modifier\n            .onFocusChanged {\n                if (it.hasFocus) onFocus()\n            },\n        onClick = onClick,\n        onLongClick = onLongClick,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surface,\n            focusedContainerColor = MaterialTheme.colorScheme.surface,\n            pressedContainerColor = MaterialTheme.colorScheme.surface\n        ),\n        tonalElevation = 8.dp,\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        scale = ClickableSurfaceDefaults.scale(scale = 1f, focusedScale = 1.04f),\n        border = ClickableSurfaceDefaults.border(\n            border = Border(\n                BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.border.copy(0.05f))\n            ),\n            focusedBorder = Border(\n                border = BorderStroke(width = 3.dp, color = MaterialTheme.colorScheme.border),\n                shape = MaterialTheme.shapes.medium\n            )\n        )\n    ) {\n        Column {\n            val coverModifier = if (coverHeight != null) {\n                Modifier.height(coverHeight)\n            } else {\n                Modifier.fillMaxWidth()\n            }\n            val textBoxModifier = if (coverHeight != null) {\n                Modifier.width((0.765 * coverHeight.value).dp)\n            } else {\n                Modifier\n            }\n\n            Box(\n                contentAlignment = Alignment.BottomCenter\n            ) {\n                AsyncImage(\n                    modifier = coverModifier\n                        .aspectRatio(0.765f)\n                        .onGloballyPositioned { coordinates ->\n                            coverRealWidth = with(localDensity) { coordinates.size.width.toDp() }\n                        },\n                    model = data.cover,\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n\n                if (data.rating != null) {\n                    Box(\n                        modifier = Modifier\n                            .height(48.dp)\n                            // 无法使用 fillMaxWidth 来确定宽度\n                            .width(coverRealWidth)\n                            .background(\n                                Brush.verticalGradient(\n                                    colors = listOf(\n                                        Color.Transparent,\n                                        Color.Black.copy(alpha = 0.8f)\n                                    )\n                                )\n                            )\n                    )\n                    Text(\n                        modifier = Modifier\n                            .align(Alignment.BottomEnd)\n                            .fillMaxWidth()\n                            .padding(8.dp, 0.dp),\n                        text = data.rating ?: \"\",\n                        fontStyle = FontStyle.Italic,\n                        fontWeight = FontWeight.Bold,\n                        fontSize = 24.sp,\n                        textAlign = TextAlign.End,\n                        color = Color.White\n                    )\n                }\n            }\n\n            Column(\n                modifier = textBoxModifier.padding(8.dp, 6.dp, 8.dp, 8.dp)\n            ) {\n                Text(\n                    text = data.title,\n                    style = MaterialTheme.typography.titleMedium,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n                if (data.subTitle != null) {\n                    Text(\n                        text = data.subTitle ?: \"\",\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        fontSize = 12.sp,\n                        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun SeasonCardPreview() {\n    BVTheme {\n        LazyVerticalGrid(columns = GridCells.Fixed(6)) {\n            repeat(6) {\n                item {\n                    SeasonCard(\n                        data = SeasonCardData(\n                            seasonId = 40794,\n                            title = \"007：没空去死\",\n                            cover = \"http://i0.hdslb.com/bfs/bangumi/image/8d211c396aad084d6fa413015200dda6ed260768.png\",\n                            rating = \"8.6\"\n                        )\n                    )\n                }\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/videocard/SmallVideoCard.kt",
    "content": "package dev.aaa1115910.bv.tv.component.videocard\n\nimport android.content.res.Configuration\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.tv.component.UpIcon\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.resizedImageUrl\n\n@Composable\nfun SmallVideoCard(\n    modifier: Modifier = Modifier,\n    data: VideoCardData,\n    onClick: () -> Unit = {},\n    onLongClick: () -> Unit = {},\n    onFocus: () -> Unit = {},\n    initialFocus: Boolean = false,\n    unfocusedBorderColor: Color? = null\n) {\n    var hasFocus by remember { mutableStateOf(initialFocus) }\n\n    Surface(\n        modifier = modifier\n            .fillMaxWidth()\n            .onFocusChanged {\n                hasFocus = it.isFocused\n                if (hasFocus) onFocus()\n            }\n            .ifElse(\n                hasFocus,\n                Modifier.border(\n                    width = 2.dp,\n                    color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f),\n                    shape = MaterialTheme.shapes.medium\n                )\n            )\n            .then(\n                if (!hasFocus && unfocusedBorderColor != null) {\n                    Modifier.border(\n                        width = 2.dp,\n                        color = unfocusedBorderColor,\n                        shape = MaterialTheme.shapes.medium\n                    )\n                } else {\n                    Modifier\n                }\n            ),\n        onClick = onClick,\n        onLongClick = onLongClick,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = Color.Transparent,\n            focusedContainerColor = if (hasFocus) MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f) else Color.Transparent,\n            pressedContainerColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f)\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        scale = ClickableSurfaceDefaults.scale(scale = 1f, focusedScale = 1f)\n    ) {\n        Column(\n            modifier = modifier\n                .fillMaxWidth()\n                .padding(top = 3.dp, start = 3.dp, end = 3.dp),\n        ) {\n            CardCover(\n                modifier = Modifier\n                    .clip(MaterialTheme.shapes.medium),\n                cover = data.cover,\n                play = data.playString,\n                danmaku = data.danmakuString,\n                time = data.timeString,\n                badge = \"${if(data.isChargingArc) \"⚡\" else \"\"}${if(data.badgeText.isEmpty() && data.isChargingArc) \"充电专属\" else data.badgeText}\"\n            )\n            Spacer(modifier = Modifier.height(8.dp))\n            CardInfo(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(79.dp)\n                    .padding(horizontal = 1.dp),\n                title = data.title,\n                upName = data.upName,\n                pubTime = data.pubTime\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun CoverBottomInfo(\n    modifier: Modifier = Modifier,\n    play: String,\n    danmaku: String,\n    time: String\n) {\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(10.dp, 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        if (play.isNotBlank()) {\n            Icon(\n                painter = painterResource(id = R.drawable.ic_play_count),\n                contentDescription = null,\n                tint = Color.White\n            )\n            Spacer(Modifier.width(2.dp))\n            Text(\n                text = play,\n                style = MaterialTheme.typography.bodySmall,\n                color = Color.White\n            )\n        }\n\n        if (danmaku.isNotBlank()) {\n            if (play.isNotBlank()) Spacer(Modifier.width(8.dp))\n            Icon(\n                painter = painterResource(id = R.drawable.ic_danmaku_count),\n                contentDescription = null,\n                tint = Color.White\n            )\n            Spacer(Modifier.width(2.dp))\n            Text(\n                text = danmaku,\n                style = MaterialTheme.typography.bodySmall,\n                color = Color.White\n            )\n        }\n\n        Spacer(Modifier.weight(1f))\n        Text(\n            text = time,\n            style = MaterialTheme.typography.bodySmall,\n            color = Color.White,\n            maxLines = 1\n        )\n    }\n}\n\n@Composable\nfun CardCover(\n    modifier: Modifier = Modifier,\n    cover: String,\n    play: String,\n    danmaku: String,\n    time: String,\n    badge: String = \"\"\n) {\n    BoxWithConstraints(\n        modifier = modifier,\n        contentAlignment = Alignment.BottomCenter\n    ) {\n        val showInfo = maxWidth > 160.dp\n\n        AsyncImage(\n            modifier = Modifier\n                .fillMaxWidth()\n                .aspectRatio(1.6f),\n            model = cover.resizedImageUrl(ImageSize.SmallVideoCardCover),\n            contentDescription = null,\n            contentScale = ContentScale.Crop\n        )\n        // 封面与徽章叠放，徽章绝对定位在右上角\n        if (badge.isNotEmpty()) {\n            Text(\n                modifier = Modifier\n                    .padding(5.dp)\n                    .align(Alignment.TopEnd)\n                    .background(\n                        color = Color.Black.copy(0.3f),\n                        shape = MaterialTheme.shapes.extraSmall\n                    )\n                    .padding(vertical = 1.dp, horizontal = 2.dp),\n                text = badge,\n                style = MaterialTheme.typography.bodySmall,\n                color = Color.White\n            )\n        }\n        // 只有需要显示时才创建阴影和信息组件\n        if (showInfo) {\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(48.dp)\n                    .background(\n                        Brush.verticalGradient(\n                            colors = listOf(\n                                Color.Transparent,\n                                Color.Black.copy(alpha = 0.8f)\n                            )\n                        )\n                    )\n            )\n\n            CoverBottomInfo(\n                play = play,\n                danmaku = danmaku,\n                time = time\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun CardInfo(\n    modifier: Modifier = Modifier,\n    title: String,\n    upName: String,\n    pubTime: String?\n) {\n    Column(modifier = modifier) {\n        Text(\n            modifier = Modifier,\n            text = title,\n            style = MaterialTheme.typography.titleMedium.copy(fontSize = 15.sp),\n            maxLines = 2,\n            minLines = 2,\n            overflow = TextOverflow.Ellipsis,\n        )\n        Spacer(modifier = Modifier.height(4.dp))\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .height(24.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(4.dp)\n        ) {\n            if (upName.isNotEmpty()) {\n                UpIcon()\n                Text(\n                    modifier = Modifier\n                        .weight(1f)\n                        .padding(end = 2.dp),\n                    text = upName,\n                    style = MaterialTheme.typography.labelMedium,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n            }\n            pubTime?.let {\n                Text(\n                    text = it,\n                    style = MaterialTheme.typography.labelSmall,\n                    maxLines = 1,\n                    overflow = TextOverflow.Visible\n                )\n            }\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SmallVideoCardWithoutFocusPreview() {\n    val data = VideoCardData(\n        avid = 0,\n        title = \"震惊！太震惊了！真的是太震惊了！我的天呐！真TMD震惊！\",\n        cover = \"http://i2.hdslb.com/bfs/archive/af17fc07b8f735e822563cc45b7b5607a491dfff.jpg\",\n        upName = \"震惊！太震惊了！真的是太震惊了！我的天呐！真TMD震惊！\",\n        play = 2333,\n        danmaku = 666,\n        time = 2333 * 1000,\n        pubTime = \"3小时前\",\n        isChargingArc = true\n    )\n    BVTheme {\n        Surface(\n            modifier = Modifier.width(300.dp)\n        ) {\n            SmallVideoCard(\n                modifier = Modifier.padding(20.dp),\n                data = data,\n                initialFocus = false\n            )\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SmallVideoCardWithFocusPreview() {\n    val data = VideoCardData(\n        avid = 0,\n        title = \"震惊！太震惊了！真的是太震惊了！我的天呐！真TMD震惊！\",\n        cover = \"http://i2.hdslb.com/bfs/archive/af17fc07b8f735e822563cc45b7b5607a491dfff.jpg\",\n        upName = \"bishi\",\n        play = 2333,\n        danmaku = 666,\n        time = 2333 * 1000,\n        pubTime = \"3小时前\"\n    )\n    BVTheme {\n        Surface(\n            modifier = Modifier.width(300.dp)\n        ) {\n            SmallVideoCard(\n                modifier = Modifier.padding(20.dp),\n                data = data,\n                initialFocus = true\n            )\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SmallVideoCardsPreview() {\n    val data = VideoCardData(\n        avid = 0,\n        title = \"震惊！太震惊了！真的是太震惊了！我的天呐！真TMD震惊！\",\n        //cover = \"http://i2.hdslb.com/bfs/archive/af17fc07b8f735e822563cc45b7b5607a491dfff.jpg\",\n        cover = \"\",\n        upName = \"bishi\",\n        play = 2333,\n        danmaku = 666,\n        time = 2333 * 1000,\n        pubTime = \"3小时前\"\n    )\n    BVTheme {\n        LazyVerticalGrid(\n            columns = GridCells.Fixed(4),\n            contentPadding = PaddingValues(24.dp),\n            verticalArrangement = Arrangement.spacedBy(12.dp),\n            horizontalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            repeat(20) {\n                item {\n                    SmallVideoCard(\n                        data = data\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/videocard/TabbedVideosPanel.kt",
    "content": "package dev.aaa1115910.bv.tv.component.videocard\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Tab\nimport androidx.tv.material3.TabDefaults\nimport androidx.tv.material3.TabRow\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.tv.util.stableItemKey\n\n@Composable\nfun TabbedVideosPanel(\n    modifier: Modifier = Modifier,\n    relatedVideos: List<VideoCardData>,\n    preloadedVideos: List<VideoCardData>,\n    currentAid: Long,\n    focusRequester: FocusRequester,\n    onOpenSeasonInfo: (VideoCardData, Boolean) -> Unit = { _, _ -> },\n    onOpenVideoInfo: (VideoCardData, Boolean) -> Unit = { _, _ -> },\n) {\n    val context = LocalContext.current\n    val density = LocalDensity.current\n    var selectedTabIndex by remember { mutableIntStateOf(0) }\n    var hasFocus by remember { mutableStateOf(false) }\n    val titleFontSize by animateFloatAsState(\n        targetValue = 24f,\n        label = \"title font size\",\n        animationSpec = tween(durationMillis = 120)\n    )\n    var rowHeight by remember { mutableStateOf(0.dp) }\n\n    // Build tabs: always show \"推荐视频\", show \"视频列表\" only when preloaded is not empty\n    val tabs = remember(relatedVideos.size, preloadedVideos.size) {\n        buildList {\n            add(\"推荐视频\" to relatedVideos)\n            if (preloadedVideos.isNotEmpty()) {\n                add(\"UGC视频列表\" to preloadedVideos)\n            }\n        }\n    }\n\n    // Clamp selectedTabIndex\n    LaunchedEffect(tabs.size) {\n        if (selectedTabIndex >= tabs.size) selectedTabIndex = 0\n    }\n\n    val currentVideos = tabs.getOrNull(selectedTabIndex)?.second ?: emptyList()\n\n    // Find index of current video in the preloaded list tab\n    val currentVideoIndexInPreloaded = remember(preloadedVideos, currentAid) {\n        preloadedVideos.indexOfFirst { it.avid == currentAid }\n    }\n\n    val relatedListState = rememberLazyListState()\n    val preloadedListState = rememberLazyListState()\n\n    // When switching to \"视频列表\" tab and current video is in the list, scroll to it\n    val isPreloadedTab = tabs.size > 1 && selectedTabIndex == 1\n    val lazyListState = if (isPreloadedTab) preloadedListState else relatedListState\n    LaunchedEffect(selectedTabIndex) {\n        if (isPreloadedTab && currentVideoIndexInPreloaded >= 0) {\n            preloadedListState.scrollToItem(currentVideoIndexInPreloaded)\n        }\n    }\n\n    val listFocusRestorer = rememberTvLazyListFocusRestorer(focusRequester)\n\n    val onLongClickVideo: (VideoCardData) -> Unit = { videoCard ->\n        if (videoCard.upId > 0)\n            UpInfoActivity.actionStart(\n                context,\n                mid = videoCard.upId,\n                name = videoCard.upName,\n                face = videoCard.upFace\n            )\n    }\n\n    Column(\n        modifier = modifier\n            .onFocusChanged { hasFocus = it.hasFocus }\n            .background(\n                Brush.verticalGradient(\n                    colors = listOf(\n                        Color.Transparent,\n                        Color.Black.copy(alpha = 0.7f)\n                    )\n                )\n            )\n    ) {\n        // Tab row - only show if more than one tab\n        if (tabs.size > 1) {\n            TabRow(\n                modifier = Modifier.padding(start = 36.dp, top = 3.dp, bottom = 3.dp),\n                selectedTabIndex = selectedTabIndex,\n                separator = { Text(\"  \") }\n            ) {\n                tabs.forEachIndexed { index, (title, _) ->\n                    Tab(\n                        selected = selectedTabIndex == index,\n                        onFocus = { selectedTabIndex = index },\n                        onClick = { selectedTabIndex = index },\n                        colors = TabDefaults.pillIndicatorTabColors(),\n                    ) {\n                        Text(\n                            text = title,\n                            fontSize = titleFontSize.sp,\n                            modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp)\n                        )\n                    }\n                }\n            }\n        } else {\n            Text(\n                modifier = Modifier.padding(start = 36.dp, top = 3.dp, bottom = 3.dp),\n                text = tabs.firstOrNull()?.first ?: \"\",\n                fontSize = titleFontSize.sp\n            )\n        }\n        LazyRow(\n            modifier = listFocusRestorer.containerModifier(\n                Modifier\n                    .padding(vertical = 15.dp)\n                    .onGloballyPositioned {\n                        rowHeight = with(density) { it.size.height.toDp() }\n                    }\n            ),\n            state = lazyListState,\n            horizontalArrangement = Arrangement.spacedBy(20.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            contentPadding = PaddingValues(horizontal = 36.dp)\n        ) {\n            itemsIndexed(\n                items = currentVideos,\n                key = { index, videoData -> \"${selectedTabIndex}-${index}-${videoData.stableItemKey()}\" }\n            ) { index, videoData ->\n                val isCurrentVideo = isPreloadedTab && index == currentVideoIndexInPreloaded\n                SmallVideoCard(\n                    modifier = listFocusRestorer.firstItemModifier(index, Modifier.width(200.dp)),\n                    data = videoData,\n                    unfocusedBorderColor = if (isCurrentVideo) MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) else null,\n                    onClick = {\n                        val fromUGCList = selectedTabIndex == 1\n                        if (videoData.jumpToSeason) {\n                            onOpenSeasonInfo(videoData, fromUGCList)\n                        } else {\n                            onOpenVideoInfo(videoData, fromUGCList)\n                        }\n                    },\n                    onLongClick = { onLongClickVideo(videoData) }\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/component/videocard/VideosRow.kt",
    "content": "package dev.aaa1115910.bv.tv.component.videocard\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.ButtonDefaults\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.tv.util.stableItemKey\nimport dev.aaa1115910.bv.util.ifElse\n\n@Composable\nfun VideosRow(\n    modifier: Modifier = Modifier,\n    header: String,\n    hideShowMore: Boolean = true,\n    videos: List<VideoCardData>,\n    showMore: () -> Unit,\n    onOpenSeasonInfo: (VideoCardData) -> Unit = {},\n    onOpenVideoInfo: (VideoCardData) -> Unit = {},\n    focusRequester: FocusRequester? = null // 渲染为 播放器-推荐视频 时有值\n) {\n    val context = LocalContext.current\n    val density = LocalDensity.current\n    val internalFocusRequester = remember { FocusRequester() }\n    val activeFocusRequester = focusRequester ?: internalFocusRequester\n    val listFocusRestorer = rememberTvLazyListFocusRestorer(activeFocusRequester)\n    var hasFocus by remember { mutableStateOf(false) }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (focusRequester != null) 24f else if (hasFocus) 30f else 14f,\n        label = \"title font size\",\n        animationSpec = tween(\n            durationMillis = 120\n        )\n    )\n    var rowHeight by remember { mutableStateOf(0.dp) }\n\n    val onLongClickVideo: (VideoCardData) -> Unit = { videoCard ->\n        if (videoCard.upId > 0)\n            UpInfoActivity.actionStart(\n                context,\n                mid = videoCard.upId,\n                name = videoCard.upName,\n                face = videoCard.upFace\n            )\n    }\n\n    Column(\n        modifier = modifier\n            .onFocusChanged { hasFocus = it.hasFocus }\n            .ifElse(focusRequester != null, Modifier.background(\n                Brush.verticalGradient(\n                    colors = listOf(\n                        Color.Transparent,\n                        Color.Black.copy(alpha = 0.7f)\n                    )\n                )\n            ))\n    ) {\n        Text(\n            modifier = Modifier.padding(start = 36.dp, top = 3.dp, bottom = 3.dp),\n            text = header,\n            fontSize = titleFontSize.sp\n        )\n        LazyRow(\n            modifier = listFocusRestorer.containerModifier(\n                Modifier\n                    .padding(vertical = 15.dp)\n                    .onGloballyPositioned {\n                        rowHeight = with(density) {\n                            it.size.height.toDp()\n                        }\n                    }\n            ),\n            horizontalArrangement = Arrangement.spacedBy(20.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            contentPadding = PaddingValues(horizontal = 36.dp)\n        ) {\n            itemsIndexed(\n                items = videos,\n                key = { index, videoData -> \"$index-${videoData.stableItemKey()}\" }\n            ) { index, videoData ->\n                SmallVideoCard(\n                    modifier = listFocusRestorer.firstItemModifier(index, Modifier.width(200.dp)),\n                    data = videoData,\n                    onClick = {\n                        if (videoData.jumpToSeason) {\n                            onOpenSeasonInfo(videoData)\n                        } else {\n                            onOpenVideoInfo(videoData)\n                        }\n                    },\n                    onLongClick={onLongClickVideo(videoData)}\n                )\n            }\n            if (!hideShowMore) {\n                item {\n                    Button(\n                        modifier = Modifier.height(rowHeight),\n                        shape = ButtonDefaults.shape(shape = MaterialTheme.shapes.medium),\n                        onClick = showMore\n                    ) {\n                        Column(\n                            modifier = Modifier.fillMaxHeight(),\n                            verticalArrangement = Arrangement.Center,\n                        ) {\n                            Text(text = \"显示更多\")\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/manager/FollowStateManager.kt",
    "content": "package dev.aaa1115910.bv.tv.manager\n\nimport dev.aaa1115910.biliapi.repositories.UserRepository\nimport dev.aaa1115910.bv.util.Prefs\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport org.koin.java.KoinJavaComponent.get\nimport java.util.concurrent.ConcurrentHashMap\n\n/**\n * 关注状态管理器，用于在不同页面间同步用户关注状态\n * 避免重复调用API获取关注状态\n */\nobject FollowStateManager {\n    // 存储用户关注状态的Map，key为用户mid，value为关注状态\n    private val _followStateMap = MutableStateFlow<Map<Long, Boolean>>(emptyMap())\n    val followStateMap: StateFlow<Map<Long, Boolean>> = _followStateMap.asStateFlow()\n\n    // 每个 mid 一把锁，保证同一个 mid 只会有一个在途请求\n    private val fetchLocks = ConcurrentHashMap<Long, Mutex>()\n\n    private val userRepository: UserRepository by lazy { get(UserRepository::class.java) }\n    \n    /**\n     * 获取指定用户的关注状态\n     * @param mid 用户mid\n     * @return 关注状态，null表示未知状态（需要调用API获取）\n     */\n    fun getFollowState(mid: Long): Boolean? {\n        return _followStateMap.value[mid]\n    }\n\n    /**\n     * 获取或请求指定用户的关注状态，自动去重。\n     * 多个调用方对同一 mid 并发调用时，只有第一个会执行 API 请求，\n     * 后续调用方等待锁释放后直接读取缓存。\n     */\n    suspend fun ensureFollowState(mid: Long): Boolean? {\n        if (mid <= 0) return null\n        getFollowState(mid)?.let { return it }\n\n        val lock = fetchLocks.getOrPut(mid) { Mutex() }\n        return lock.withLock {\n            // 拿到锁后再查一次缓存，前一个持锁者可能已经写入\n            getFollowState(mid)?.let { return@withLock it }\n\n            val result = runCatching {\n                userRepository.checkIsFollowing(\n                    mid = mid,\n                    preferApiType = Prefs.apiType\n                )\n            }.getOrNull()\n            if (result != null) {\n                updateFollowState(mid, result)\n            }\n            result\n        }\n    }\n    \n    /**\n     * 更新用户关注状态\n     * @param mid 用户mid\n     * @param isFollowing 是否关注\n     */\n    fun updateFollowState(mid: Long, isFollowing: Boolean) {\n        _followStateMap.value = _followStateMap.value.toMutableMap().apply {\n            this[mid] = isFollowing\n        }\n    }\n    \n    /**\n     * 移除指定用户的关注状态缓存\n     * @param mid 用户mid\n     */\n    fun removeFollowState(mid: Long) {\n        _followStateMap.value = _followStateMap.value.toMutableMap().apply {\n            remove(mid)\n        }\n    }\n    \n    /**\n     * 清空所有关注状态缓存\n     */\n    fun clearAll() {\n        _followStateMap.value = emptyMap()\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/manager/PlayedAidsCache.kt",
    "content": "package dev.aaa1115910.bv.tv.manager\n\nimport java.util.concurrent.ConcurrentHashMap\n\n/**\n * Application 级播放过的稿件 aid 缓存。\n * 用途：避免自动播放推荐时出现重复稿件；在退出播放器或应用重启时清空。\n */\nobject PlayedAidsCache {\n    // 使用线程安全集合，保证多协程访问安全\n    private val playedAids = ConcurrentHashMap.newKeySet<Long>()\n\n    /** 标记已播放 */\n    fun markPlayed(aid: Long) {\n        if (aid > 0) playedAids.add(aid)\n    }\n\n    /** 是否已经播放过 */\n    fun hasPlayed(aid: Long): Boolean = aid > 0 && playedAids.contains(aid)\n\n    /** 返回所有已播放 aid 快照 */\n    fun all(): Set<Long> = playedAids.toSet()\n\n    /** 清空缓存 */\n    fun clear() {\n        playedAids.clear()\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/manager/VideoUserActionManager.kt",
    "content": "package dev.aaa1115910.bv.tv.manager\n\nimport dev.aaa1115910.biliapi.entity.FavoriteFolderMetadata\nimport dev.aaa1115910.biliapi.repositories.CoinRepository\nimport dev.aaa1115910.biliapi.repositories.FavoriteRepository\nimport dev.aaa1115910.biliapi.repositories.LikeRepository\nimport dev.aaa1115910.biliapi.repositories.ToViewRepository\nimport dev.aaa1115910.bv.util.Prefs\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withContext\nimport org.koin.java.KoinJavaComponent.get\nimport java.util.concurrent.ConcurrentHashMap\n\ndata class VideoActionState(\n    val liked: Boolean = false,\n    val favorited: Boolean = false,\n    val coin: Boolean = false,\n    val favoriteFolderIds: List<Long> = emptyList()\n)\n\n/**\n * Simple in-memory manager for video user actions (like/favorite/coin).\n * Keyed by aid. Exposes a StateFlow per aid so UI can collect and share state across screens.\n * Network operations delegate to repositories from Koin and update the corresponding flow on success.\n */\nobject VideoUserActionManager {\n    // key = Pair(uid, aid)\n    private val stateMap = ConcurrentHashMap<Pair<Long, Long>, MutableStateFlow<VideoActionState>>()\n    // key = uid, favorite folders are user-global\n    private val favoriteFoldersMap = ConcurrentHashMap<Long, MutableStateFlow<List<FavoriteFolderMetadata>>>()\n    private val fetchMutexMap = ConcurrentHashMap<Long, Mutex>()\n    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)\n\n    private fun key(uid: Long, aid: Long) = uid to aid\n\n    private fun ensure(aid: Long, uid: Long = Prefs.uid): MutableStateFlow<VideoActionState> {\n        val k = key(uid, aid)\n        stateMap[k]?.let { return it }\n        val newFlow = MutableStateFlow(VideoActionState())\n        return stateMap.putIfAbsent(k, newFlow) ?: newFlow\n    }\n\n    fun getStateFlow(aid: Long, uid: Long = Prefs.uid): StateFlow<VideoActionState> = ensure(aid, uid)\n\n    private fun ensureFavoriteFolders(uid: Long = Prefs.uid): MutableStateFlow<List<FavoriteFolderMetadata>> {\n        val flow = favoriteFoldersMap.getOrPut(uid) { MutableStateFlow(emptyList()) }\n        if (flow.value.isEmpty()) {\n            scope.launch {\n                val mutex = fetchMutexMap.getOrPut(uid) { Mutex() }\n                mutex.withLock {\n                    if (flow.value.isNotEmpty()) return@launch\n                    runCatching {\n                        flow.value = get<FavoriteRepository>(FavoriteRepository::class.java)\n                            .getAllFavoriteFolderMetadataList(mid = uid, preferApiType = Prefs.apiType)\n                    }\n                }\n            }\n        }\n        return flow\n    }\n\n    fun getFavoriteFoldersFlow(uid: Long = Prefs.uid): StateFlow<List<FavoriteFolderMetadata>> = ensureFavoriteFolders(uid)\n\n    suspend fun updateFromLoadedData(aid: Long, liked: Boolean, favorited: Boolean, coin: Boolean, uid: Long = Prefs.uid) {\n        val flow = ensure(aid, uid)\n        flow.value = flow.value.copy(liked = liked, favorited = favorited, coin = coin)\n\n        // load favorite folder ids for this video\n        if (aid <= 0 || !Prefs.isLogin) return\n        val favoriteRepository: FavoriteRepository = get(FavoriteRepository::class.java)\n        val mutex = fetchMutexMap.getOrPut(uid) { Mutex() }\n        mutex.withLock {\n            runCatching {\n                val folders = withContext(Dispatchers.IO) {\n                    favoriteRepository.getAllFavoriteFolderMetadataList(\n                        mid = uid,\n                        rid = aid,\n                        preferApiType = Prefs.apiType\n                    )\n                }\n                // update folder list cache (superset of the no-rid call)\n                favoriteFoldersMap.getOrPut(uid) { MutableStateFlow(emptyList()) }.value = folders\n                // update video action state with folder ids\n                val folderIds = folders.filter { it.videoInThisFav }.map { it.id }\n                flow.value = flow.value.copy(favoriteFolderIds = folderIds)\n            }\n        }\n    }\n\n    suspend fun addLike(aid: Long, uid: Long = Prefs.uid): Boolean {\n        if (aid <= 0) return false\n        val likeRepository: LikeRepository = get(LikeRepository::class.java)\n        return try {\n            withContext(Dispatchers.IO) { likeRepository.addVideoLike(aid = aid) }\n            ensure(aid, uid).value = ensure(aid, uid).value.copy(liked = true)\n            true\n        } catch (_: Exception) {\n            false\n        }\n    }\n\n    suspend fun delLike(aid: Long, uid: Long = Prefs.uid): Boolean {\n        if (aid <= 0) return false\n        val likeRepository: LikeRepository = get(LikeRepository::class.java)\n        return try {\n            withContext(Dispatchers.IO) { likeRepository.delVideoLike(aid = aid) }\n            ensure(aid, uid).value = ensure(aid, uid).value.copy(liked = false)\n            true\n        } catch (_: Exception) {\n            false\n        }\n    }\n\n    suspend fun addCoin(aid: Long, uid: Long = Prefs.uid): Boolean {\n        if (aid <= 0) return false\n        val coinRepository: CoinRepository = get(CoinRepository::class.java)\n        return try {\n            withContext(Dispatchers.IO) { coinRepository.addVideoCoin(aid = aid) }\n            ensure(aid, uid).value = ensure(aid, uid).value.copy(coin = true)\n            true\n        } catch (_: Exception) {\n            false\n        }\n    }\n\n    suspend fun addToView(aid: Long, uid: Long = Prefs.uid): Boolean {\n        if (aid <= 0 || uid <= 0) return false\n        val toViewRepository: ToViewRepository = get(ToViewRepository::class.java)\n        return try {\n            withContext(Dispatchers.IO) {\n                toViewRepository.addToView(\n                    avid = aid,\n                    preferApiType = Prefs.apiType\n                )\n            }\n        } catch (_: Exception) {\n            false\n        }\n    }\n\n    suspend fun updateVideoFavoriteFolders(aid: Long, folderIds: List<Long>, uid: Long = Prefs.uid): Boolean {\n        if (aid <= 0) return false\n        val favoriteRepository: FavoriteRepository = get(FavoriteRepository::class.java)\n        val currentFolders = ensureFavoriteFolders(uid).value\n        return try {\n            withContext(Dispatchers.IO) {\n                require(currentFolders.isNotEmpty())\n                favoriteRepository.updateVideoToFavoriteFolder(\n                    aid = aid,\n                    addMediaIds = folderIds,\n                    delMediaIds = currentFolders.map { it.id } - folderIds.toSet()\n                )\n            }\n            ensure(aid, uid).value = ensure(aid, uid).value.copy(\n                favoriteFolderIds = folderIds,\n                favorited = folderIds.isNotEmpty()\n            )\n            true\n        } catch (_: Exception) {\n            false\n        }\n    }\n\n    suspend fun delVideoFromFavoriteFolder(aid: Long, folderId: Long, uid: Long = Prefs.uid): Boolean {\n        if (aid <= 0) return false\n        val favoriteRepository: FavoriteRepository = get(FavoriteRepository::class.java)\n        return try {\n            withContext(Dispatchers.IO) {\n                favoriteRepository.delVideoFromFavoriteFolder(\n                    aid = aid,\n                    delMediaIds = listOf(folderId),\n                    preferApiType = Prefs.apiType\n                )\n            }\n            val flow = ensure(aid, uid)\n            val updatedIds = flow.value.favoriteFolderIds - folderId\n            flow.value = flow.value.copy(\n                favoriteFolderIds = updatedIds,\n                favorited = updatedIds.isNotEmpty()\n            )\n            true\n        } catch (_: Exception) {\n            false\n        }\n    }\n\n    suspend fun addToDefaultFavoriteFolder(aid: Long, uid: Long = Prefs.uid): Boolean {\n        if (aid <= 0) return false\n        val flow = ensure(aid, uid)\n        val default = ensureFavoriteFolders(uid).value.firstOrNull { it.title == \"默认收藏夹\" }\n            ?: return false\n        return updateVideoFavoriteFolders(aid, listOf(default.id), uid)\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/MainScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens\n\nimport android.app.Activity\nimport android.content.Intent\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.NavigationRail\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.NavSwitchMode\nimport dev.aaa1115910.bv.tv.component.UserPanel\nimport dev.aaa1115910.bv.tv.activities.settings.SettingsActivity\nimport dev.aaa1115910.bv.tv.activities.user.FavoriteActivity\nimport dev.aaa1115910.bv.tv.activities.user.FollowingSeasonActivity\nimport dev.aaa1115910.bv.tv.activities.user.HistoryActivity\nimport dev.aaa1115910.bv.tv.activities.user.LoginActivity\nimport dev.aaa1115910.bv.tv.activities.user.ToViewActivity\nimport dev.aaa1115910.bv.tv.activities.user.UserInfoActivity\nimport dev.aaa1115910.bv.tv.screens.main.DrawerContent\nimport dev.aaa1115910.bv.tv.screens.main.DrawerItem\nimport dev.aaa1115910.bv.tv.screens.main.HomeContent\nimport dev.aaa1115910.bv.tv.screens.main.LiveContent\nimport dev.aaa1115910.bv.tv.screens.main.PgcContent\nimport dev.aaa1115910.bv.tv.screens.main.UgcContent\nimport dev.aaa1115910.bv.tv.screens.main.currentSelectedTabs\nimport dev.aaa1115910.bv.tv.screens.main.drawerItemFocusRequesters\nimport dev.aaa1115910.bv.tv.screens.search.SearchInputScreen\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fException\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.UserViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.delay\nimport org.koin.androidx.compose.koinViewModel\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\n@Composable\nfun MainScreen(\n    modifier: Modifier = Modifier,\n    userViewModel: UserViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val logger = KotlinLogging.logger(\"MainScreen\")\n    val scope = rememberCoroutineScope()\n    var showUserPanel by remember { mutableStateOf(false) }\n    var lastPressBack: Long by remember { mutableLongStateOf(0L) }\n    var selectedDrawerItem by remember { mutableStateOf(DrawerItem.Home) }\n    var focusedDrawerItem by remember { mutableStateOf(DrawerItem.Home) }\n    val navSwitchMode by Prefs.navSwitchModeFlow.collectAsState(Prefs.navSwitchMode)\n\n    val mainFocusRequester = remember { FocusRequester() }\n    val ugcFocusRequester = remember { FocusRequester() }\n    val pgcFocusRequester = remember { FocusRequester() }\n    val liveFocusRequester = remember { FocusRequester() }\n    val searchFocusRequester = remember { FocusRequester() }\n\n    // 时间显示状态\n    var currentTime by remember {\n        val dateFormat = SimpleDateFormat(\"HH:mm\", Locale.getDefault())\n        mutableStateOf(dateFormat.format(Date()))\n    }\n\n    // 定时更新时间\n    LaunchedEffect(Unit) {\n        val dateFormat = SimpleDateFormat(\"HH:mm\", Locale.getDefault())\n        while (true) {\n            delay(60_000L - System.currentTimeMillis() % 60_000L)\n            currentTime = dateFormat.format(Date())\n        }\n    }\n\n    val handleBack = {\n        val currentTime = System.currentTimeMillis()\n        if (currentTime - lastPressBack < 1500) {\n            logger.fInfo { \"Exiting bug video\" }\n            currentSelectedTabs[DrawerItem.Home] = Prefs.defaultHomeTab\n            (context as Activity).finish()\n        } else {\n            lastPressBack = currentTime\n            R.string.home_press_back_again_to_exit.toast(context)\n        }\n    }\n\n    val onFocusToContent: () -> Unit = {\n        when (selectedDrawerItem) {\n            DrawerItem.Home -> mainFocusRequester.requestFocus()\n            DrawerItem.UGC -> ugcFocusRequester.requestFocus()\n            DrawerItem.PGC -> pgcFocusRequester.requestFocus()\n            DrawerItem.Live -> liveFocusRequester.requestFocus()\n            DrawerItem.Search -> searchFocusRequester.requestFocus()\n            else -> {\n                // 用户+设置等非内容页，回到当前选中内容的菜单项再进入内容\n                drawerItemFocusRequesters[selectedDrawerItem]?.requestFocus()\n            }\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        runCatching {\n            mainFocusRequester.requestFocus()\n        }.onFailure {\n            logger.fException(it) { \"request default focus requester failed\" }\n        }\n    }\n\n    BackHandler {\n        handleBack()\n    }\n\n    Scaffold(modifier = modifier) { contentPadding ->\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(contentPadding)\n        ) {\n            val borderColor = MaterialTheme.colorScheme.surfaceContainerHigh\n            val borderWidth = 1.dp\n            // Left side - NavigationRail\n            NavigationRail(\n                modifier = Modifier\n                    .align(Alignment.CenterStart)\n                    .width(71.dp)\n                    .padding(end = borderWidth)\n                    .drawBehind {\n                        val borderWidthPx = borderWidth.toPx()\n                        val x = size.width + borderWidthPx\n\n                        drawLine(\n                            color = borderColor,\n                            start = Offset(x = x, y = 0f),\n                            end = Offset(x = x, y = size.height),\n                            strokeWidth = borderWidthPx\n                        )\n                    },\n            ) {\n                DrawerContent(\n                    modifier = Modifier.fillMaxWidth(),\n                    isLogin = userViewModel.isLogin,\n                    avatar = userViewModel.face,\n                    username = userViewModel.username,\n                    navSwitchMode = navSwitchMode,\n                    //avatar = \"https://i2.hdslb.com/bfs/face/ef0457addb24141e15dfac6fbf45293ccf1e32ab.jpg\",\n                    //username = \"碧诗\",\n                    onDrawerItemChanged = { selectedDrawerItem = it },\n                    onDrawerItemfocused = {\n                        focusedDrawerItem = it\n                    },\n                    onOpenSettings = {\n                        context.startActivity(Intent(context, SettingsActivity::class.java))\n                    },\n                    onShowUserPanel = {\n                        // showUserPanel = true\n                        context.startActivity(Intent(context, UserInfoActivity::class.java))\n                    },\n                    onFocusToContent = onFocusToContent,\n                    onLogin = {\n                        context.startActivity(Intent(context, LoginActivity::class.java))\n                    }\n                )\n            }\n\n            // Right side - NavHost content\n            AnimatedContent(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(start = 72.dp),\n                targetState = selectedDrawerItem,\n                label = \"main animated content\",\n                transitionSpec = {\n                    val coefficient = 20\n                    if (targetState.ordinal < initialState.ordinal) {\n                        slideInVertically { -it / coefficient } togetherWith\n                                fadeOut(animationSpec = tween(200)) + slideOutVertically { it / coefficient }\n                    } else {\n                        slideInVertically { it / coefficient } togetherWith\n                                fadeOut(animationSpec = tween(200)) + slideOutVertically { -it / coefficient }\n                    }\n                }\n            ) { screen ->\n                when (screen) {\n                    DrawerItem.Home -> HomeContent(navFocusRequester = mainFocusRequester)\n                    DrawerItem.UGC -> UgcContent(navFocusRequester = ugcFocusRequester)\n                    DrawerItem.PGC -> PgcContent(navFocusRequester = pgcFocusRequester)\n                    DrawerItem.Live -> LiveContent(navFocusRequester = liveFocusRequester)\n                    DrawerItem.Search -> SearchInputScreen(defaultFocusRequester = searchFocusRequester)\n                    else -> {}\n                }\n            }\n\n            // 右上角时间显示\n            Text(\n                text = currentTime,\n                modifier = Modifier\n                    .align(Alignment.TopEnd)\n                    .padding(end = 8.dp, top = 0.dp)\n                    .offset(y=(-2).dp),\n                style = MaterialTheme.typography.titleMedium.copy(\n                    fontSize = 13.sp\n                ),\n                color = MaterialTheme.colorScheme.onSurface\n            )\n        }\n        AnimatedVisibility(\n            visible = showUserPanel,\n            enter = fadeIn(),\n            exit = fadeOut()\n        ) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .background(Color.Black.copy(alpha = 0.6f))\n            ) {\n                AnimatedVisibility(\n                    modifier = Modifier\n                        .align(Alignment.Center),\n                    visible = showUserPanel,\n                    enter = fadeIn() + scaleIn(),\n                    exit = fadeOut()\n                ) {\n                    UserPanel(\n                        modifier = Modifier\n                            .padding(12.dp),\n                        username = userViewModel.username,\n                        face = userViewModel.face,\n                        onHide = { showUserPanel = false },\n                        onGoMy = {\n                            context.startActivity(Intent(context, UserInfoActivity::class.java))\n                        },\n                        onGoHistory = {\n                            context.startActivity(Intent(context, HistoryActivity::class.java))\n                        },\n                        onGoFavorite = {\n                            context.startActivity(Intent(context, FavoriteActivity::class.java))\n                        },\n                        onGoFollowing = {\n                            context.startActivity(\n                                Intent(\n                                    context,\n                                    FollowingSeasonActivity::class.java\n                                )\n                            )\n                        },\n                        onGoLater = {\n                            context.startActivity(Intent(context, ToViewActivity::class.java))\n                        }\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/RegionBlockScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens\n\nimport android.app.Activity\nimport android.content.res.Configuration\nimport android.graphics.BitmapFactory\nimport androidx.compose.animation.core.animateIntAsState\nimport androidx.compose.animation.core.keyframes\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.ImageBitmapConfig\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport okhttp3.internal.toHexString\nimport qrcode.QRCode\nimport java.io.ByteArrayInputStream\nimport java.io.ByteArrayOutputStream\nimport kotlin.system.exitProcess\n\n@Composable\nfun RegionBlockScreen(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n    var qrImage by remember { mutableStateOf(ImageBitmap(1, 1, ImageBitmapConfig.Argb8888)) }\n    val primaryColorHex =\n        \"#\" + MaterialTheme.colorScheme.surface.toArgb().toHexString().substring(2)\n\n    var finishNumberTarget by remember { mutableIntStateOf(0) }\n    val finishNumber by animateIntAsState(\n        targetValue = finishNumberTarget,\n        animationSpec = keyframes {\n            durationMillis = 12 * 1000\n            0 at 0\n            10 at 1 * 1000\n            60 at 3 * 1000\n            90 at 7 * 1000\n            91 at 10 * 1000\n            100 at 12 * 1000\n        },\n        label = \"finish percent animation\"\n    )\n\n    LaunchedEffect(Unit) {\n        println(primaryColorHex)\n        val output = ByteArrayOutputStream()\n        finishNumberTarget = 100\n        QRCode(context.getString(R.string.region_block_qr_content))\n            .render()\n            .writeImage(output)\n        val input = ByteArrayInputStream(output.toByteArray())\n        qrImage = BitmapFactory.decodeStream(input).asImageBitmap()\n    }\n\n    DisposableEffect(key1 = Unit) {\n        onDispose {\n            (context as Activity).finish()\n            exitProcess(0)\n        }\n    }\n\n    Surface(\n        modifier = modifier\n            .focusable()\n            .onKeyEvent {\n                (context as Activity).finish()\n                exitProcess(0)\n            },\n        shape = RoundedCornerShape(0.dp)\n    ) {\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(84.dp),\n            contentAlignment = Alignment.CenterStart\n        ) {\n            Column(\n                verticalArrangement = Arrangement.spacedBy(24.dp)\n            ) {\n                Text(\n                    text = stringResource(R.string.region_block_character_painting),\n                    fontSize = 100.sp\n                )\n                Column {\n                    Text(\n                        text = stringResource(R.string.region_block_title),\n                        style = MaterialTheme.typography.titleLarge\n                    )\n                    Text(\n                        text = stringResource(R.string.region_block_subtitle_tv),\n                        style = MaterialTheme.typography.titleLarge\n                    )\n                }\n                Text(\n                    text = \"$finishNumber% 完成\",\n                    style = MaterialTheme.typography.titleLarge\n                )\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(12.dp)\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .size(80.dp)\n                            .background(Color.White),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        Image(\n                            modifier = Modifier.size(64.dp),\n                            bitmap = qrImage,\n                            contentDescription = null\n                        )\n                    }\n                    Column {\n                        Text(text = stringResource(R.string.region_block_solution_title))\n                        Text(text = stringResource(R.string.region_block_solution_text))\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun RegionBlockScreenPreview() {\n    BVTheme {\n        RegionBlockScreen()\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/SeasonInfoScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens\n\nimport android.app.Activity\nimport android.content.res.Configuration\nimport android.os.Build\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.relocation.BringIntoViewRequester\nimport androidx.compose.foundation.relocation.bringIntoViewRequester\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.ViewModule\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.BlendMode\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalInspectionMode\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport androidx.tv.material3.Border\nimport androidx.tv.material3.Card\nimport androidx.tv.material3.CardDefaults\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.Glow\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.LocalContentColor\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Tab\nimport androidx.tv.material3.TabRow\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.video.season.Episode\nimport dev.aaa1115910.biliapi.entity.video.season.PgcSeason\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.player.entity.VideoListPgcEpisode\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.component.CommentPanel\nimport dev.aaa1115910.bv.tv.component.LoadingTip\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.buttons.SeasonInfoButtons\nimport dev.aaa1115910.bv.tv.util.launchPlayerActivity\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.focusedScale\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.onBackPressed\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.util.resizedImageUrl\nimport dev.aaa1115910.bv.util.swapList\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.SeasonViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.koinInject\nimport kotlin.math.ceil\n\n/**\n * 生成剧集标题\n * @param episode 剧集信息\n * @param sectionTitle 所属章节标题\n * @return 格式化后的剧集标题\n */\nprivate fun generateEpisodeTitle(\n    episode: Episode?,\n    sectionTitle: String\n): String {\n    if(episode == null) return \"\"\n\n    return if (episode.longTitle.isNotEmpty()) {\n        runCatching {\n            \"第 ${episode.title.toInt()} 集 \"\n        }.getOrDefault(\"\") + episode.longTitle\n    } else if (sectionTitle == \"正片\") {\n        //如果 title 是数字的话，就会返回 \"第 x 集\"\n        //如果 title 不是数字的话（例如 SP），就会原样使用 title\n        runCatching {\n            \"第 ${episode.title.toInt()} 集\"\n        }.getOrDefault(episode.title)\n    } else {\n        episode.title\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SeasonInfoScreen(\n    modifier: Modifier = Modifier,\n    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,\n    videoInfoRepository: VideoInfoRepository = koinInject(),\n    seasonViewModel: SeasonViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val intent = (context as Activity).intent\n    val logger = KotlinLogging.logger { }\n\n    var paused by remember { mutableStateOf(false) }\n    var showSeasonSelector by remember { mutableStateOf(false) }\n    var showCommentPanel by remember { mutableStateOf(false) }\n    val commentButtonFocusRequester = remember { FocusRequester() }\n    val playButtonFocusRequester = remember { FocusRequester() }\n\n    val onClickVideo: (avid: Long, cid: Long, epid: Int, episodeTitle: String, startTime: Int) -> Unit =\n        { avid, cid, epid, episodeTitle, startTime ->\n            logger.debug { \"onClickVideo: [avid=$avid, cid=$cid, epid=$epid, episodeTitle=$episodeTitle, startTime=$startTime]\" }\n            if (cid != 0L) {\n                videoInfoRepository.description = seasonViewModel.seasonData?.description ?: \"\"\n                videoInfoRepository.tags = emptyList()\n                launchPlayerActivity(\n                    context = context,\n                    avid = avid,\n                    cid = cid,\n                    title = seasonViewModel.seasonData!!.title,\n                    partTitle = episodeTitle,\n                    played = startTime * 1000,\n                    fromSeason = true,\n                    subType = seasonViewModel.seasonData?.subType,\n                    epid = epid,\n                    seasonId = seasonViewModel.seasonData?.seasonId,\n                    proxyArea = seasonViewModel.proxyArea,\n                    playerIconIdle = seasonViewModel.seasonData?.playerIcon?.idle ?: \"\",\n                    playerIconMoving = seasonViewModel.seasonData?.playerIcon?.moving ?: \"\"\n                )\n            } else {\n                //如果 cid==0，就需要跳转回 VideoInfoActivity 去获取 cid 再跳转播放器\n                VideoInfoActivity.actionStart(\n                    context = context,\n                    aid = avid,\n                    fromSeason = true\n                )\n            }\n        }\n\n    val onClickFollow: (Boolean) -> Unit = {\n        scope.launch(Dispatchers.IO) {\n            if (seasonViewModel.isFollowing) seasonViewModel.unFollowSeason() else seasonViewModel.followSeason()\n        }\n    }\n\n    val onClickCover = {\n        if (seasonViewModel.seasonData?.seasons?.isNotEmpty() == true) showSeasonSelector = true\n    }\n\n    val onShowComment = {\n        showCommentPanel = true\n    }\n\n    val getCommentAid = {\n        val lastEpId = seasonViewModel.lastPlayProgress?.lastEpId\n        if (lastEpId != null) {\n            // 查找最后播放的剧集\n            seasonViewModel.seasonData?.episodes?.find { it.id == lastEpId }?.aid\n                ?: seasonViewModel.seasonData?.sections?.mapNotNull { section ->\n                    section.episodes.find { it.id == lastEpId }?.aid\n                }?.firstOrNull()\n        } else {\n            // 没有播放记录，使用第一集\n            seasonViewModel.seasonData?.episodes?.firstOrNull()?.aid\n        } ?: 0L\n    }\n\n    LaunchedEffect(Unit) {\n        videoInfoRepository.relatedVideos.clear()\n        \n        val epId = intent.getIntExtra(\"epid\", 0)\n        val seasonId = intent.getIntExtra(\"seasonid\", 0)\n        val proxyAreaIndex = intent.getIntExtra(\"proxy_area\", 0)\n        val proxyArea = ProxyArea.entries[proxyAreaIndex]\n        logger.fInfo { \"Read extras from content: [epId=$epId, seasonId=$seasonId, proxyArea=$proxyArea]\" }\n\n        seasonViewModel.epId = epId\n        seasonViewModel.seasonId = seasonId\n        seasonViewModel.proxyArea = proxyArea\n\n        if (seasonViewModel.epId != null || seasonViewModel.seasonId != null) {\n            scope.launch(Dispatchers.IO) {\n                seasonViewModel.updateSeasonData()\n            }\n        } else {\n            context.finish()\n        }\n    }\n\n    LaunchedEffect(seasonViewModel.seasonData) {\n        seasonViewModel.seasonData?.let {\n            logger.fInfo { \"season data change: ${seasonViewModel.seasonData}\" }\n            seasonViewModel.lastPlayProgress = it.userStatus.progress\n            //请求默认焦点到播放按钮上\n            delay(300)\n            playButtonFocusRequester.requestFocus(scope)\n        }\n    }\n\n    DisposableEffect(lifecycleOwner) {\n        val observer = LifecycleEventObserver { _, event ->\n            if (event == Lifecycle.Event.ON_PAUSE) {\n                paused = true\n            } else if (event == Lifecycle.Event.ON_RESUME) {\n                // 如果 pause==true 那可能是从播放页返回回来的，此时更新历史记录\n                if (paused) {\n                    scope.launch(Dispatchers.IO) {\n                        seasonViewModel.updateLastPlayProgress()\n                    }\n                }\n            }\n        }\n\n        lifecycleOwner.lifecycle.addObserver(observer)\n\n        onDispose {\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n\n    if (seasonViewModel.seasonData == null) {\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .background(MaterialTheme.colorScheme.surface),\n            contentAlignment = Alignment.Center\n\n        ) {\n            if (seasonViewModel.tip == \"Loading\") {\n                LoadingTip()\n            } else {\n                Text(\n                    text = seasonViewModel.tip\n                )\n            }\n        }\n    } else {\n        val seasonData = seasonViewModel.seasonData!!\n        Scaffold(\n            modifier = modifier\n        ) { innerPadding ->\n            LazyColumn(\n                modifier = Modifier\n                    .padding(innerPadding)\n                    .fillMaxSize(),\n                contentPadding = PaddingValues(vertical = 16.dp),\n                verticalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                item {\n                    SeasonInfoPart(\n                        playButtonFocusRequester = playButtonFocusRequester,\n                        title = seasonData.title,\n                        cover = seasonData.cover,\n                        newEpDesc = seasonData.newEpDesc,\n                        description = seasonData.description,\n                        lastPlayedIndex = seasonViewModel.lastPlayProgress?.lastEpId ?: -1,\n                        lastPlayedTitle = generateEpisodeTitle(seasonData.episodes.find { it.id == seasonViewModel.lastPlayProgress?.lastEpId }, seasonData.title),\n                        following = seasonViewModel.isFollowing,\n                        isPublished = seasonData.publish.isPublished,\n                        publishDate = seasonData.publish.publishDate,\n                        seasonCount = seasonData.seasons.size,\n                        onPlay = {\n                            logger.fInfo { \"Click play button\" }\n                            var playAid = -1L\n                            var playCid = -1L\n                            val playEpid: Int\n                            var episodeList: List<Episode> = emptyList()\n                            if (seasonViewModel.lastPlayProgress == null) {\n                                logger.fInfo { \"Didn't find any play record\" }\n                                //未登录或无播放记录，此时lastPlayProgress==null，默认播放第一集正片\n                                playAid = seasonViewModel.seasonData?.episodes?.first()?.aid ?: -1\n                                playCid = seasonViewModel.seasonData?.episodes?.first()?.cid ?: -1\n                                playEpid = seasonViewModel.seasonData?.episodes?.first()?.id ?: -1\n                                if (playCid == -1L) {\n                                    R.string.season_no_feature_film.toast(context)\n                                } else {\n                                    episodeList =\n                                        seasonViewModel.seasonData?.episodes ?: emptyList()\n                                }\n                            } else {\n                                //已登录且有播放记录\n                                logger.fInfo { \"Find play record: ${seasonViewModel.lastPlayProgress}\" }\n\n                                //懒得去改播放器那边来支持epid，就直接在这边查找cid了\n                                playEpid = seasonViewModel.lastPlayProgress!!.lastEpId\n                                seasonViewModel.seasonData?.episodes?.forEach {\n                                    if (it.id == playEpid) {\n                                        playAid = it.aid\n                                        playCid = it.cid\n                                        episodeList =\n                                            seasonViewModel.seasonData?.episodes ?: emptyList()\n                                    }\n                                }\n                                if (playCid == -1L) {\n                                    seasonViewModel.seasonData?.sections?.forEach { section ->\n                                        section.episodes.forEach {\n                                            if (it.id == playEpid) {\n                                                playAid = it.aid\n                                                playCid = it.cid\n                                                episodeList = section.episodes\n                                            }\n                                        }\n                                    }\n                                }\n                                if (playCid == -1L) {\n                                    logger.fInfo { \"Can't find cid\" }\n                                    \"无法判断最后播放的剧集\".toast(context)\n                                }\n                            }\n\n                            logger.fInfo { \"Play aid: $playAid, cid: $playCid\" }\n                            val lastEpId = seasonViewModel.lastPlayProgress?.lastEpId\n                            val ep = seasonViewModel.seasonData?.episodes?.find { it.id == lastEpId } ?: seasonViewModel.seasonData?.episodes?.find { it.cid == playCid }\n                            if (playCid != -1L) {\n                                onClickVideo(\n                                    playAid,\n                                    playCid,\n                                    playEpid,\n                                    generateEpisodeTitle(ep, seasonViewModel.seasonData!!.title),\n                                    seasonViewModel.lastPlayProgress?.lastTime ?: 0\n                                )\n\n                                val partVideoList = episodeList.mapIndexed { index, episode ->\n                                    VideoListPgcEpisode(\n                                        aid = episode.aid,\n                                        cid = episode.cid,\n                                        epid = episode.id,\n                                        seasonId = seasonViewModel.seasonData?.seasonId,\n                                        title = seasonViewModel.seasonData!!.title,\n                                        partTitle = runCatching {\n                                            \"第 ${episode.title.toInt()} 集\"\n                                        }.getOrDefault(episode.title) + \" \" + episode.longTitle,\n                                        index = index,\n                                        cover = episode.cover,\n                                        duration = episode.duration / 1000,\n                                        pubDate = episode.pubDate\n                                    )\n                                }\n                                videoInfoRepository.videoList.clear()\n                                videoInfoRepository.videoList.addAll(partVideoList)\n                            }\n                        },\n                        onClickFollow = onClickFollow,\n                        onClickCover = onClickCover,\n                        onShowComment = onShowComment,\n                        commentButtonFocusRequester = commentButtonFocusRequester\n                    )\n                }\n                if (seasonViewModel.seasonData?.episodes?.isNotEmpty() == true) {\n                    item {\n                        SeasonEpisodeRow(\n                            title = stringResource(R.string.season_feature_film),\n                            episodes = seasonViewModel.seasonData?.episodes ?: emptyList(),\n                            lastPlayedId = seasonViewModel.lastPlayProgress?.lastEpId ?: 0,\n                            lastPlayedTime = seasonViewModel.lastPlayProgress?.lastTime ?: 0,\n                            onClick = { avid, cid, epid, episodeTitle, startTime ->\n                                onClickVideo(avid, cid, epid, episodeTitle, startTime)\n\n                                val partVideoList =\n                                    seasonViewModel.seasonData?.episodes?.mapIndexed { index, episode ->\n                                        VideoListPgcEpisode(\n                                            aid = episode.aid,\n                                            cid = episode.cid,\n                                            epid = episode.id,\n                                            seasonId = seasonViewModel.seasonData?.seasonId,\n                                            title = seasonViewModel.seasonData!!.title,\n                                            partTitle = runCatching {\n                                                \"第 ${episode.title.toInt()} 集\"\n                                            }.getOrDefault(episode.title) + \" \" + episode.longTitle,\n                                            index = index,\n                                            cover = episode.cover,\n                                            duration = episode.duration,\n                                        pubDate = episode.pubDate\n                                        )\n                                    } ?: emptyList()\n                                videoInfoRepository.videoList.clear()\n                                videoInfoRepository.videoList.addAll(partVideoList)\n                            }\n                        )\n                    }\n                }\n                seasonViewModel.seasonData?.sections?.forEach { section ->\n                    item {\n                        SeasonEpisodeRow(\n                            title = section.title,\n                            episodes = section.episodes,\n                            lastPlayedId = seasonViewModel.lastPlayProgress?.lastEpId ?: 0,\n                            lastPlayedTime = seasonViewModel.lastPlayProgress?.lastTime ?: 0,\n                            onClick = { avid, cid, epid, episodeTitle, startTime ->\n                                onClickVideo(avid, cid, epid, episodeTitle, startTime)\n\n                                val partVideoList = section.episodes.mapIndexed { index, episode ->\n                                    VideoListPgcEpisode(\n                                        aid = episode.aid,\n                                        cid = episode.cid,\n                                        epid = episode.id,\n                                        seasonId = seasonViewModel.seasonData?.seasonId,\n                                        title = runCatching {\n                                            \"第 ${episode.title.toInt()} 集\"\n                                        }.getOrDefault(episode.title) + \" \" + episode.longTitle,\n                                        index = index,\n                                        cover = episode.cover,\n                                        duration = episode.duration,\n                                        pubDate = episode.pubDate\n                                    )\n                                }\n                                videoInfoRepository.videoList.clear()\n                                videoInfoRepository.videoList.addAll(partVideoList)\n                            }\n                        )\n                    }\n                }\n                item {\n                    Spacer(modifier = Modifier.height(64.dp))\n                }\n            }\n        }\n    }\n\n    SeasonSelector(\n        show = showSeasonSelector,\n        onHideSelector = {\n            showSeasonSelector = false\n            runCatching {\n                playButtonFocusRequester.requestFocus(scope)\n            }\n        },\n        currentSeasonId = seasonViewModel.seasonId ?: 0,\n        seasons = seasonViewModel.seasonData?.seasons ?: emptyList(),\n        onClickSeason = { sid ->\n            if ((seasonViewModel.seasonId ?: 0) != sid) {\n                seasonViewModel.seasonData = null\n                seasonViewModel.seasonId = sid\n                seasonViewModel.epId = null\n                scope.launch(Dispatchers.IO) { seasonViewModel.updateSeasonData() }\n            }\n        }\n    )\n\n    CommentPanel(\n        show = showCommentPanel,\n        oid = getCommentAid(),\n        onHide = {\n            showCommentPanel = false\n            commentButtonFocusRequester.requestFocus(scope)\n        },\n        episodes = seasonViewModel.seasonData?.episodes ?: emptyList(),\n        sections = seasonViewModel.seasonData?.sections ?: emptyList(),\n        initialEpisodeId = seasonViewModel.lastPlayProgress?.lastEpId ?: -1,\n        onEpisodeChange = { episode ->\n            logger.debug { \"User viewed comments for episode: ${episode.id} (${episode.title})\" }\n        }\n    )\n}\n\n@Composable\nfun SeasonCover(\n    modifier: Modifier = Modifier,\n    cover: String,\n    seasonCount: Int,\n    onClick: () -> Unit\n) {\n    val isPreview = LocalInspectionMode.current\n    var hasFocus by remember { mutableStateOf(false) }\n    val coverBottomTipHeight by animateDpAsState(\n        targetValue = if (hasFocus && seasonCount != 0) 28.dp else 0.dp,\n        label = \"Cover bottom tip height\"\n    )\n\n    Card(\n        modifier = modifier.onFocusChanged { hasFocus = it.hasFocus },\n        onClick = onClick,\n        shape = CardDefaults.shape(shape = MaterialTheme.shapes.medium),\n        glow = CardDefaults.glow(\n            focusedGlow = Glow(\n                elevationColor = MaterialTheme.colorScheme.inverseSurface,\n                elevation = 16.dp\n            )\n        ),\n        border = if (Build.VERSION.SDK_INT < 31) {\n            CardDefaults.border()\n        } else {\n            CardDefaults.border(\n                focusedBorder = Border(BorderStroke(0.dp, Color.Transparent))\n            )\n        }\n    ) {\n        Box {\n            AsyncImage(\n                modifier = Modifier\n                    .height(260.dp)\n                    .aspectRatio(0.75f)\n                    .background(if (isPreview) Color.White else Color.Transparent),\n                model = cover,\n                contentDescription = null,\n                contentScale = ContentScale.FillHeight\n            )\n\n            Row(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .width(195.dp)\n                    .height(coverBottomTipHeight)\n                    .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)),\n                horizontalArrangement = Arrangement.Center,\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Text(\n                    modifier = Modifier,\n                    text = stringResource(R.string.season_count_tip, seasonCount),\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun SeasonBaseInfo(\n    modifier: Modifier = Modifier,\n    title: String,\n    newEpDesc: String,\n    description: String,\n    lastPlayedIndex: Int,\n    lastPlayedTitle: String = \"\",\n    following: Boolean,\n    isPublished: Boolean,\n    publishDate: String,\n    onPlay: () -> Unit,\n    onClickFollow: (follow: Boolean) -> Unit,\n    onShowComment: () -> Unit = {},\n    commentButtonFocusRequester: FocusRequester = remember { FocusRequester() },\n    playButtonFocusRequester: FocusRequester\n) {\n    Column(\n        modifier = modifier\n            .heightIn(min = 260.dp),\n        verticalArrangement = Arrangement.SpaceBetween\n    ) {\n        Column(\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.titleLarge,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                color = MaterialTheme.colorScheme.onSurface\n            )\n            Text(text = newEpDesc)\n            Text(text = description)\n        }\n        Spacer(modifier = Modifier.height(12.dp))\n        SeasonInfoButtons(\n            lastPlayedIndex = lastPlayedIndex,\n            lastPlayedTitle = lastPlayedTitle,\n            following = following,\n            isPublished = isPublished,\n            publishDate = publishDate,\n            onPlay = onPlay,\n            onClickFollow = onClickFollow,\n            onShowComment= onShowComment,\n            commentButtonFocusRequester = commentButtonFocusRequester,\n            playButtonFocusRequester = playButtonFocusRequester,\n        )\n    }\n}\n\n@Composable\nfun SeasonInfoPart(\n    modifier: Modifier = Modifier,\n    title: String,\n    cover: String,\n    newEpDesc: String,\n    description: String,\n    lastPlayedIndex: Int,\n    lastPlayedTitle: String = \"\",\n    following: Boolean,\n    isPublished: Boolean,\n    publishDate: String,\n    seasonCount: Int,\n    onPlay: () -> Unit,\n    onClickFollow: (follow: Boolean) -> Unit,\n    onClickCover: () -> Unit,\n    onShowComment: () -> Unit = {},\n    commentButtonFocusRequester: FocusRequester = remember { FocusRequester() },\n    playButtonFocusRequester: FocusRequester\n) {\n    Row(\n        modifier = modifier\n            .padding(horizontal = 32.dp, vertical = 16.dp),\n        horizontalArrangement = Arrangement.spacedBy(24.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        SeasonCover(\n            cover = cover,\n            seasonCount = seasonCount,\n            onClick = onClickCover\n        )\n        SeasonBaseInfo(\n            title = title,\n            newEpDesc = newEpDesc,\n            description = description,\n            lastPlayedIndex = lastPlayedIndex,\n            lastPlayedTitle = lastPlayedTitle,\n            following = following,\n            isPublished = isPublished,\n            publishDate = publishDate,\n            onPlay = onPlay,\n            onClickFollow = onClickFollow,\n            onShowComment = onShowComment,\n            commentButtonFocusRequester = commentButtonFocusRequester,\n            playButtonFocusRequester = playButtonFocusRequester\n        )\n    }\n}\n\n\n@Composable\nfun SeasonEpisodeButton(\n    modifier: Modifier = Modifier,\n    partTitle: String = \"\",\n    title: String,\n    cover: String,\n    duration: Int,\n    played: Int = 0,\n    isLastPlayed: Boolean = false,\n    onClick: () -> Unit\n) {\n    val isPreview = LocalInspectionMode.current\n\n    Surface(\n        modifier = modifier,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surfaceVariant,\n            focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n            pressedContainerColor = MaterialTheme.colorScheme.inverseSurface\n        ),\n        scale = ClickableSurfaceDefaults.scale(scale = 1f, focusedScale = 1f),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        border = ClickableSurfaceDefaults.border(\n            border = if (isLastPlayed) {\n                Border(\n                    border = BorderStroke(2.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.35f)),\n                    shape = MaterialTheme.shapes.medium\n                )\n            } else Border.None,\n            focusedBorder = Border(\n                border = BorderStroke(2.dp, MaterialTheme.colorScheme.border),\n                shape = MaterialTheme.shapes.medium\n            ),\n            pressedBorder = Border(\n                border = BorderStroke(2.dp, MaterialTheme.colorScheme.border),\n                shape = MaterialTheme.shapes.medium\n            )\n        ),\n        onClick = onClick\n    ) {\n        Row {\n            val coverBackground by remember { mutableStateOf(if (played != 0) Color.Black.copy(alpha = 0.2f) else Color.Transparent) }\n            Box(\n                modifier = Modifier.background(coverBackground)\n            ) {\n                AsyncImage(\n                    modifier = Modifier\n                        .height(80.dp)\n                        .aspectRatio(1.6f)\n                        .clip(MaterialTheme.shapes.medium)\n                        .background(if (isPreview) Color.White else Color.Transparent),\n                    model = cover.resizedImageUrl(ImageSize.Cover),\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n            }\n\n            Box(\n                modifier = Modifier\n                    .size(140.dp, 80.dp)\n            ) {\n                Box(\n                    modifier = Modifier\n                        .background(Color.Black.copy(alpha = 0.2f))\n                        .fillMaxHeight()\n                        .fillMaxWidth(if (duration == 0) 0f else if (played < 0) 1f else ((played * 1000f) / duration))\n                ) {}\n                Column(\n                    modifier = Modifier\n                        .fillMaxHeight()\n                        .padding(horizontal = 8.dp),\n                    verticalArrangement = Arrangement.SpaceAround\n                ) {\n                    Box(\n                        modifier = Modifier.weight(1f),\n                        contentAlignment = Alignment.CenterStart\n                    ) {\n                        Text(text = partTitle)\n                    }\n                    Box(\n                        modifier = Modifier.weight(2f),\n                        contentAlignment = Alignment.TopStart\n                    ) {\n                        Text(\n                            text = title,\n                            maxLines = 2,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun SeasonEpisodesDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    title: String,\n    episodes: List<Episode>,\n    lastPlayedId: Int = 0,\n    lastPlayedTime: Int = 0,\n    onHideDialog: () -> Unit,\n    onClick: (avid: Long, cid: Long, epid: Int, episodeTitle: String, startTime: Int) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n\n    var selectedTabIndex by remember { mutableIntStateOf(0) }\n    val tabCount by remember { mutableIntStateOf(ceil(episodes.size / 20.0).toInt()) }\n    val selectedEpisodes = remember { mutableStateListOf<Episode>() }\n\n    val tabFocusRequester = remember { FocusRequester() }\n    val tabRowFocusRequester = remember { FocusRequester() }\n    val videoListFocusRequester = remember { FocusRequester() }\n    val listState = rememberLazyGridState()\n\n    LaunchedEffect(selectedTabIndex) {\n        val fromIndex = selectedTabIndex * 20\n        var toIndex = (selectedTabIndex + 1) * 20\n        if (toIndex >= episodes.size) {\n            toIndex = episodes.size\n        }\n        selectedEpisodes.swapList(episodes.subList(fromIndex, toIndex))\n    }\n\n    LaunchedEffect(show) {\n        if (show && tabCount > 1) tabFocusRequester.requestFocus(scope)\n        if (show && tabCount == 1) videoListFocusRequester.requestFocus(scope)\n    }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            title = { Text(text = title) },\n            onDismissRequest = { onHideDialog() },\n            confirmButton = {},\n            properties = DialogProperties(usePlatformDefaultWidth = false),\n            text = {\n                Column(\n                    modifier = Modifier\n                        .size(600.dp, 330.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    // TabRow 只有一项 Tab 时会导致崩溃，但如果只有一项 Tab 的时候也没必要显示\n                    // https://issuetracker.google.com/issues/264018028\n                    if (tabCount > 1) {\n                        TabRow(\n                            modifier = Modifier\n                                .onFocusChanged {\n                                    if (it.hasFocus) {\n                                        scope.launch(Dispatchers.Main) {\n                                            listState.scrollToItem(0)\n                                        }\n                                    }\n                                }\n                                .focusRestorer()\n                                .focusRequester(tabRowFocusRequester),\n                            selectedTabIndex = selectedTabIndex,\n                            separator = { Spacer(modifier = Modifier.width(12.dp)) },\n                        ) {\n                            for (i in 0 until tabCount) {\n                                Tab(\n                                    modifier = if (i == 0) Modifier.focusRequester(\n                                        tabFocusRequester\n                                    ) else Modifier,\n                                    selected = i == selectedTabIndex,\n                                    onFocus = { selectedTabIndex = i },\n                                ) {\n                                    Text(\n                                        text = \"P${i * 20 + 1}-${(i + 1) * 20}\",\n                                        fontSize = 12.sp,\n                                        color = LocalContentColor.current,\n                                        modifier = Modifier.padding(\n                                            horizontal = 16.dp,\n                                            vertical = 6.dp\n                                        )\n                                    )\n                                }\n                            }\n                        }\n                    }\n\n                    LazyVerticalGrid(\n                        modifier = Modifier\n                            .onBackPressed {\n                                if (tabCount > 1) tabRowFocusRequester.requestFocus() else onHideDialog()\n                            },\n                        state = listState,\n                        columns = GridCells.Fixed(2),\n                        contentPadding = PaddingValues(8.dp),\n                        verticalArrangement = Arrangement.spacedBy(8.dp),\n                    ) {\n                        itemsIndexed(\n                            items = selectedEpisodes,\n                            key = { index, episode -> \"$index-episode-${episode.aid}-${episode.cid}\" }\n                        ) { index, episode ->\n                            val episodeTitle by remember { mutableStateOf(generateEpisodeTitle(episode, title)) }\n                            val buttonModifier =\n                                if (index == 0) Modifier.focusRequester(videoListFocusRequester) else Modifier\n                            SeasonEpisodeButton(\n                                modifier = buttonModifier\n                                    .focusedScale(0.95f),\n                                partTitle = if (title == \"正片\") {\n                                    //如果 title 是数字的话，就会返回 \"第 x 集\"\n                                    //如果 title 不是数字的话（例如 SP），就会原样使用 title\n                                    runCatching {\n                                        \"第 ${episode.title.toInt()} 集\"\n                                    }.getOrDefault(episode.title)\n                                } else {\n                                    \"P${index + 1 + selectedTabIndex * 20}\"\n                                },\n                                title = episodeTitle,\n                                cover = episode.cover,\n                                played = if (episode.id == lastPlayedId) lastPlayedTime else 0,\n                                isLastPlayed = episode.id == lastPlayedId,\n                                duration = episode.duration,\n                                onClick = {\n                                    onClick(\n                                        episode.aid,\n                                        episode.cid,\n                                        episode.id,\n                                        generateEpisodeTitle(episode, title),\n                                        if (episode.id == lastPlayedId) lastPlayedTime else 0\n                                    )\n                                }\n                            )\n                        }\n                    }\n                }\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun SeasonEpisodeRowButton(\n    modifier: Modifier = Modifier,\n    hasFocus: Boolean = true,\n    onClick: () -> Unit\n) {\n    val scale by animateFloatAsState(\n        targetValue = if (hasFocus) 1f else 0.4f,\n        label = \"button scale\",\n        animationSpec = tween(\n            durationMillis = 120\n        )\n    )\n\n    Surface(\n        modifier = modifier,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surfaceVariant,\n            focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n            pressedContainerColor = MaterialTheme.colorScheme.inverseSurface\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.small),\n        border = ClickableSurfaceDefaults.border(\n            focusedBorder = Border(\n                border = BorderStroke(2.dp, MaterialTheme.colorScheme.border),\n                shape = MaterialTheme.shapes.small\n            )\n        ),\n        onClick = onClick\n    ) {\n        Box(\n            modifier = Modifier\n                .size(width = (40 * scale).dp, height = (42 * scale).dp),\n            contentAlignment = Alignment.Center\n        ) {\n            Icon(\n                modifier = Modifier\n                    .size(32.dp)\n                    .rotate(90f),\n                imageVector = Icons.Rounded.ViewModule,\n                contentDescription = null\n            )\n        }\n    }\n}\n\n@Composable\nfun SeasonEpisodeRow(\n    modifier: Modifier = Modifier,\n    title: String,\n    episodes: List<Episode>,\n    lastPlayedId: Int = 0,\n    lastPlayedTime: Int = 0,\n    onClick: (avid: Long, cid: Long, epid: Int, episodeTitle: String, startTime: Int) -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n    val rowState = rememberLazyListState()\n    var hasFocus by remember { mutableStateOf(false) }\n    val titleColor = if (hasFocus) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (hasFocus) 30f else 14f,\n        label = \"title font size\"\n    )\n\n    var showEpisodesDialog by remember { mutableStateOf(false) }\n\n    // 当存在历史记录时，滚动到对应集\n    LaunchedEffect(lastPlayedId, episodes) {\n        if (lastPlayedId != 0 && episodes.isNotEmpty()) {\n            val lastPlayedIndex = episodes.indexOfFirst { it.id == lastPlayedId }\n            if (lastPlayedIndex != -1) {\n                rowState.scrollToItem(lastPlayedIndex)\n            }\n        }\n    }\n\n    Column(\n        modifier = modifier\n            .onFocusChanged { hasFocus = it.hasFocus },\n        verticalArrangement = Arrangement.SpaceBetween\n    ) {\n        Row(\n            modifier = Modifier.padding(start = 50.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Text(\n                text = title,\n                fontSize = titleFontSize.sp,\n                color = titleColor\n            )\n            SeasonEpisodeRowButton(\n                hasFocus = hasFocus,\n                onClick = { showEpisodesDialog = true }\n            )\n        }\n\n        LazyRow(\n            modifier = Modifier\n                .padding(top = 15.dp)\n                .focusRestorer(focusRequester),\n            state = rowState,\n            contentPadding = PaddingValues(horizontal = 32.dp),\n            horizontalArrangement = Arrangement.spacedBy(24.dp),\n        ) {\n            itemsIndexed(\n                items = episodes,\n                key = { index, episode -> \"$index-episode-${episode.id}\" }\n            ) { index, episode ->\n                val episodeTitle by remember { mutableStateOf(if (episode.longTitle != \"\") episode.longTitle else episode.title) }\n                SeasonEpisodeButton(\n                    modifier = Modifier\n                        .ifElse(index == 0, Modifier.focusRequester(focusRequester)),\n                    partTitle = if (title == \"正片\") {\n                        //如果 title 是数字的话，就会返回 \"第 x 集\"\n                        //如果 title 不是数字的话（例如 SP），就会原样使用 title\n                        runCatching {\n                            \"第 ${episode.title.toInt()} 集\"\n                        }.getOrDefault(episode.title)\n                    } else {\n                        \"P${index + 1}\"\n                    },\n                    title = episodeTitle,\n                    cover = episode.cover,\n                    played = if (episode.id == lastPlayedId) lastPlayedTime else 0,\n                    isLastPlayed = episode.id == lastPlayedId,\n                    duration = episode.duration,\n                    onClick = {\n                        val pTitle = generateEpisodeTitle(episode, title)\n                        onClick(\n                            episode.aid,\n                            episode.cid,\n                            episode.id,\n                            pTitle,\n                            if (episode.id == lastPlayedId) lastPlayedTime else 0\n                        )\n                    }\n                )\n            }\n        }\n    }\n\n    SeasonEpisodesDialog(\n        show = showEpisodesDialog,\n        title = title,\n        episodes = episodes,\n        lastPlayedId = lastPlayedId,\n        lastPlayedTime = lastPlayedTime,\n        onHideDialog = { showEpisodesDialog = false },\n        onClick = onClick\n    )\n}\n\n@Composable\nfun SeasonSelector(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideSelector: () -> Unit,\n    currentSeasonId: Int,\n    seasons: List<PgcSeason>,\n    onClickSeason: (Int) -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(show) {\n        if (show) {\n            focusRequester.requestFocus()\n        }\n    }\n\n    if (show) {\n        SeasonSelectorContent(\n            modifier = modifier\n                .focusRequester(focusRequester),\n            seasons = seasons,\n            currentSeasonId = currentSeasonId,\n            onClickSeason = { seasonId ->\n                onClickSeason(seasonId)\n                onHideSelector()\n            }\n        )\n    }\n\n    BackHandler(show) {\n        onHideSelector()\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nprivate fun SeasonSelectorContent(\n    modifier: Modifier = Modifier,\n    currentSeasonId: Int,\n    seasons: List<PgcSeason>,\n    onClickSeason: (Int) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val rowState = rememberLazyListState()\n    val logger = KotlinLogging.logger {}\n    val currentSeasonFocusRequester = remember { FocusRequester() }\n    val bringIntoViewRequester = remember { BringIntoViewRequester() }\n\n    var scrolling by remember { mutableStateOf(false) }\n    var currentSeasonIndex by remember { mutableIntStateOf(0) }\n    val isCurrentSeasonInScreen by remember {\n        derivedStateOf {\n            rowState.layoutInfo.visibleItemsInfo.first().index <= currentSeasonIndex\n                    && rowState.layoutInfo.visibleItemsInfo.last().index >= currentSeasonIndex\n        }\n    }\n\n    val scrollToCurrentSeason = {\n        currentSeasonIndex = seasons.indexOfFirst { it.seasonId == currentSeasonId }\n        logger.info { \"Season row scroll to index $currentSeasonIndex\" }\n        if (currentSeasonIndex != -1) {\n            if (isCurrentSeasonInScreen) {\n                currentSeasonFocusRequester.requestFocus()\n            } else {\n                scope.launch {\n                    scrolling = true\n                    rowState.scrollToItem(currentSeasonIndex)\n                }\n            }\n        }\n    }\n\n    LaunchedEffect(rowState.firstVisibleItemScrollOffset) {\n        if (scrolling && isCurrentSeasonInScreen) {\n            scrolling = false\n            delay(300)\n            currentSeasonFocusRequester.requestFocus()\n        }\n    }\n\n    Surface(\n        modifier = modifier\n            .fillMaxSize()\n            .onFocusChanged {\n                if (it.hasFocus) scrollToCurrentSeason()\n            },\n        shape = RoundedCornerShape(0.dp)\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize()\n        ) {\n            Box(\n                modifier = Modifier.fillMaxSize()\n            ) {\n                AsyncImage(\n                    modifier = Modifier\n                        .align(Alignment.TopEnd)\n                        .fillMaxHeight(0.7f)\n                        .graphicsLayer { alpha = 0.99f }\n                        .drawWithContent {\n                            val colors = listOf(\n                                Color.Black,\n                                Color.Transparent\n                            )\n                            drawContent()\n                            drawRect(\n                                brush = Brush.horizontalGradient(colors),\n                                blendMode = BlendMode.DstOut\n                            )\n                            drawRect(\n                                brush = Brush.verticalGradient(colors),\n                                blendMode = BlendMode.DstIn\n                            )\n                        },\n                    model = seasons[currentSeasonIndex].horizontalCover ?: \"\",\n                    contentDescription = null,\n                    contentScale = ContentScale.FillHeight,\n                    alpha = 1f\n                )\n                Column(\n                    modifier = Modifier\n                        .align(Alignment.BottomStart)\n                        .padding(\n                            start = 48.dp,\n                            end = 48.dp,\n                            bottom = 300.dp\n                        )\n                ) {\n                    Text(\n                        text = seasons[currentSeasonIndex].title\n                            ?: seasons[currentSeasonIndex].shortTitle,\n                        style = MaterialTheme.typography.displayMedium\n                    )\n                }\n            }\n\n            Box(\n                modifier = Modifier.align(Alignment.BottomStart)\n            ) {\n                LazyRow(\n                    modifier = Modifier.padding(bottom = 48.dp),\n                    state = rowState,\n                    contentPadding = PaddingValues(horizontal = 48.dp),\n                    horizontalArrangement = Arrangement.spacedBy(24.dp)\n                ) {\n                    itemsIndexed(\n                        items = seasons,\n                        key = { index, season -> \"$index-season-${season.seasonId}\" }\n                    ) { index, season ->\n                        Card(\n                            modifier = Modifier\n                                .onFocusChanged {\n                                    if (it.hasFocus) currentSeasonIndex = index\n                                }\n                                .ifElse(\n                                    season.seasonId == currentSeasonId,\n                                    Modifier.focusRequester(currentSeasonFocusRequester)\n                                )\n                                .ifElse(\n                                    season.seasonId == currentSeasonId,\n                                    Modifier.bringIntoViewRequester(bringIntoViewRequester)\n                                ),\n                            glow = CardDefaults.glow(\n                                focusedGlow = Glow(\n                                    elevationColor = MaterialTheme.colorScheme.inverseSurface,\n                                    elevation = 16.dp\n                                )\n                            ),\n                            border = if (Build.VERSION.SDK_INT < 31) {\n                                CardDefaults.border()\n                            } else {\n                                CardDefaults.border(\n                                    focusedBorder = Border(BorderStroke(0.dp, Color.Transparent))\n                                )\n                            },\n                            onClick = {\n                                onClickSeason(season.seasonId)\n                            }\n                        ) {\n                            AsyncImage(\n                                modifier = Modifier\n                                    .width(160.dp)\n                                    .aspectRatio(0.75f),\n                                model = seasons[index].cover,\n                                contentDescription = null\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SeasonInfoPartPreview() {\n    BVTheme {\n        SeasonInfoPart(\n            modifier = Modifier.fillMaxWidth(),\n            title = \"人生一串\",\n            cover = \"http://i0.hdslb.com/bfs/bangumi/7a790c64ff70f12c11888be0532b6981a923afd5.jpg\",\n            newEpDesc = \"已完结, 全8集\",\n            description = \"由bilibili和旗帜传媒联合出品的《人生一串》是国内首档汇聚民间烧烤美食，呈现国人烧烤情结的深夜美食纪录片，本片将镜头伸向街头巷尾，讲述平民美食和市井传奇，以最独特的视角真实展现烧烤美食背后的独特情感。作为一档接地气的美食节目，《串》旨在展现每一串烧烤的魅力往事，和最真实的美味体验。\",\n            lastPlayedIndex = 3,\n            lastPlayedTitle = \"拯救灵依计划\",\n            following = false,\n            isPublished = true,\n            publishDate = \"2021-04-30\",\n            seasonCount = 0,\n            onPlay = {},\n            onClickFollow = {},\n            onClickCover = {},\n            playButtonFocusRequester = remember { FocusRequester() }\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun SeasonEpisodeRowPreview() {\n    val episodes = remember { mutableStateListOf<Episode>() }\n    for (i in 0..10) {\n        episodes.add(\n            Episode(\n                id = 0,\n                aid = 0,\n                bvid = \"\",\n                cid = 0,\n                epid = 1000 + i,\n                title = \"这可能是我这辈子距离梅西最近的一次\",\n                longTitle = \"\",\n                cover = \"\",\n                duration = 0,\n                dimension = null,\n                pages = emptyList()\n            )\n        )\n    }\n    BVTheme {\n        SeasonEpisodeRow(\n            title = \"正片\",\n            episodes = episodes,\n            onClick = { _, _, _, _, _ -> }\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun SeasonSelectorPreview() {\n    val seasons = listOf(\n        PgcSeason(\n            seasonId = 25210,\n            title = \"命运之夜  06版\",\n            shortTitle = \"FATE TV\",\n            cover = \"http://i0.hdslb.com/bfs/bangumi/1113d844ad3a9b42af576d80142146cbecc1b7ff.jpg\",\n            horizontalCover = \"http://i0.hdslb.com/bfs/bangumi/e3993240914c3d881d97e4527a52efa2a9dcdeaf.jpg\"\n        ),\n        PgcSeason(\n            seasonId = 29006,\n            title = \"Fate/stay night UNLIMITED BLADE WORKS\",\n            shortTitle = \"UBW 剧场版\",\n            cover = \"http://i0.hdslb.com/bfs/bangumi/image/b7ee578ff3c258f173587db3f687fa2d56e3b8c1.jpg\",\n            horizontalCover = \"http://i0.hdslb.com/bfs/archive/ae6fcc22f6c627a899bfe2d736765cc83cd4e827.png\"\n        ),\n        PgcSeason(\n            seasonId = 1586,\n            title = \"Fate/stay night [Unlimited Blade Works] 第一季\",\n            shortTitle = \"UBW第一季\",\n            cover = \"http://i0.hdslb.com/bfs/bangumi/image/e67e09c9e48a32371a81100e0f65a61b18aabb24.png\",\n            horizontalCover = \"http://i0.hdslb.com/bfs/bangumi/25e9da6dd71e4aaa23a7dc04b6f97a94ea1ddd9d.jpg\"\n        ),\n        PgcSeason(\n            seasonId = 25210,\n            title = \"命运之夜  06版\",\n            shortTitle = \"FATE TV\",\n            cover = \"http://i0.hdslb.com/bfs/bangumi/1113d844ad3a9b42af576d80142146cbecc1b7ff.jpg\",\n            horizontalCover = \"http://i0.hdslb.com/bfs/bangumi/e3993240914c3d881d97e4527a52efa2a9dcdeaf.jpg\"\n        ),\n        PgcSeason(\n            seasonId = 29006,\n            title = \"Fate/stay night UNLIMITED BLADE WORKS\",\n            shortTitle = \"UBW 剧场版\",\n            cover = \"http://i0.hdslb.com/bfs/bangumi/image/b7ee578ff3c258f173587db3f687fa2d56e3b8c1.jpg\",\n            horizontalCover = \"http://i0.hdslb.com/bfs/archive/ae6fcc22f6c627a899bfe2d736765cc83cd4e827.png\"\n        ),\n        PgcSeason(\n            seasonId = 1586,\n            title = \"Fate/stay night [Unlimited Blade Works] 第一季\",\n            shortTitle = \"UBW第一季\",\n            cover = \"http://i0.hdslb.com/bfs/bangumi/image/e67e09c9e48a32371a81100e0f65a61b18aabb24.png\",\n            horizontalCover = \"http://i0.hdslb.com/bfs/bangumi/25e9da6dd71e4aaa23a7dc04b6f97a94ea1ddd9d.jpg\"\n        ),\n    )\n    BVTheme {\n        SeasonSelectorContent(\n            seasons = seasons,\n            currentSeasonId = 25210,\n            onClickSeason = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/TagScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens\n\nimport android.app.Activity\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.tv.util.stableItemKey\nimport dev.aaa1115910.bv.viewmodel.TagViewModel\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.koinInject\n\n@Composable\nfun TagScreen(\n    modifier: Modifier = Modifier,\n    tagViewModel: TagViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val videoInfoRepository: VideoInfoRepository = koinInject()\n    val listFocusRestorer = rememberTvLazyListFocusRestorer()\n    var currentIndex by remember { mutableIntStateOf(0) }\n    val showLargeTitle by remember { derivedStateOf { currentIndex < 4 } }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n\n    LaunchedEffect(Unit) {\n        val intent = (context as Activity).intent\n        if (intent.hasExtra(\"tagId\")) {\n            val tagId = intent.getIntExtra(\"tagId\", 0)\n            val tagName = intent.getStringExtra(\"tagName\") ?: \"\"\n            tagViewModel.tagId = tagId\n            tagViewModel.tagName = tagName\n            tagViewModel.update()\n        } else {\n            context.finish()\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            Box(\n                modifier = Modifier.padding(start = 48.dp, top = 24.dp, bottom = 8.dp, end = 48.dp)\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.Bottom,\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Text(\n                        text = tagViewModel.tagName,\n                        fontSize = titleFontSize.sp\n                    )\n                    Row(\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        Text(\n                            text = stringResource(\n                                R.string.load_data_count,\n                                tagViewModel.topVideos.size\n                            ),\n                            color = Color.White.copy(alpha = 0.6f)\n                        )\n                        AnimatedVisibility(visible = tagViewModel.noMore) {\n                            Text(\n                                text = stringResource(R.string.load_data_no_more),\n                                color = Color.White.copy(alpha = 0.6f)\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    ) { innerPadding ->\n        ProvideListBringIntoViewSpec(padding = 26.dp) {\n            LazyVerticalGrid(\n                modifier = listFocusRestorer.containerModifier(\n                    Modifier\n                        .padding(innerPadding)\n                        .blockDownFocusExitAtGridEnd(\n                            currentIndex = currentIndex,\n                            itemCount = tagViewModel.topVideos.size,\n                            columnCount = 4\n                        )\n                ),\n                columns = GridCells.Fixed(4),\n                contentPadding = PaddingValues(20.dp),\n                verticalArrangement = Arrangement.spacedBy(20.dp),\n                horizontalArrangement = Arrangement.spacedBy(20.dp)\n            ) {\n                itemsIndexed(\n                    items = tagViewModel.topVideos,\n                    key = { index, video -> \"$index-${video.stableItemKey()}\" }\n                ) { index, video ->\n                    Box(\n                        contentAlignment = Alignment.Center\n                    ) {\n                        SmallVideoCard(\n                            modifier = listFocusRestorer.firstItemModifier(index),\n                            data = video,\n                            onClick = {\n                                videoInfoRepository.preloadedVideoList.clear()\n                                videoInfoRepository.preloadedVideoList.addAll(tagViewModel.topVideos)\n                                VideoInfoActivity.actionStart(context, video.avid)\n                            },\n                            onLongClick = { UpInfoActivity.actionStart( context, mid = video.upId, name = video.upName, face = video.upFace ) },\n                            onFocus = {\n                                currentIndex = index\n                                if (index + 20 > tagViewModel.topVideos.size) {\n                                    tagViewModel.update()\n                                }\n                            }\n                        )\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/VideoInfoScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens\n\nimport android.app.Activity\nimport android.content.res.Configuration\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.gestures.animateScrollBy\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Add\nimport androidx.compose.material.icons.rounded.Done\nimport androidx.compose.material.icons.rounded.ViewModule\nimport androidx.compose.material.icons.rounded.Warning\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalView\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport androidx.tv.material3.Border\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.ExperimentalTvMaterial3Api\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.LocalContentColor\nimport androidx.tv.material3.LocalTextStyle\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.SuggestionChip\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.SurfaceDefaults\nimport androidx.tv.material3.Tab\nimport androidx.tv.material3.TabRow\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport coil.request.ImageRequest\nimport coil.transform.BlurTransformation\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.FavoriteFolderMetadata\nimport dev.aaa1115910.biliapi.entity.video.Dimension\nimport dev.aaa1115910.biliapi.entity.video.Tag\nimport dev.aaa1115910.biliapi.entity.video.VideoDetail\nimport dev.aaa1115910.biliapi.entity.video.VideoPage\nimport dev.aaa1115910.biliapi.entity.video.season.Episode\nimport dev.aaa1115910.biliapi.http.BiliPlusHttpApi\nimport dev.aaa1115910.biliapi.repositories.UserRepository\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.player.entity.VideoListItem\nimport dev.aaa1115910.bv.player.entity.VideoListPart\nimport dev.aaa1115910.bv.player.entity.VideoListUgcEpisode\nimport dev.aaa1115910.bv.player.entity.VideoListUgcEpisodeTitle\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport dev.aaa1115910.bv.tv.activities.video.SeasonInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.TagActivity\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.component.CommentPanel\nimport dev.aaa1115910.bv.tv.component.LoadingTip\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.UpIcon\nimport dev.aaa1115910.bv.tv.component.buttons.LikeButton\nimport dev.aaa1115910.bv.tv.component.buttons.CoinButton\nimport dev.aaa1115910.bv.tv.component.buttons.FavoriteButton\nimport dev.aaa1115910.bv.tv.manager.VideoUserActionManager\nimport dev.aaa1115910.bv.tv.manager.VideoUserActionManager.getStateFlow\nimport dev.aaa1115910.bv.tv.component.videocard.VideosRow\nimport dev.aaa1115910.bv.tv.util.launchPlayerActivity\nimport dev.aaa1115910.bv.tv.manager.FollowStateManager\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.fWarn\nimport dev.aaa1115910.bv.util.focusedBorder\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport dev.aaa1115910.bv.util.formatPubTimeString\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.resizedImageUrl\nimport dev.aaa1115910.bv.util.onBackPressed\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.util.swapList\nimport dev.aaa1115910.bv.util.swapListWithMainContext\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.video.VideoDetailViewModel\nimport dev.aaa1115910.bv.viewmodel.video.VideoInfoState\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.getKoin\nimport java.util.Date\nimport kotlin.math.ceil\n\n@Composable\nfun VideoInfoScreen(\n    modifier: Modifier = Modifier,\n    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,\n    videoInfoRepository: VideoInfoRepository = getKoin().get(),\n    videoDetailViewModel: VideoDetailViewModel = koinViewModel(),\n    userRepository: UserRepository = getKoin().get(),\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val screenScope = lifecycleOwner.lifecycleScope\n    val intent = (context as Activity).intent\n    val logger = KotlinLogging.logger { }\n    val defaultFocusRequester = remember { FocusRequester() }\n    val lazyListState = rememberLazyListState()\n\n    var showFollowButton by remember { mutableStateOf(false) }\n    var isFollowing by remember { mutableStateOf(false) }\n\n    // 监听关注状态变化\n    val followStateMap by FollowStateManager.followStateMap.collectAsState()\n\n    // 当关注状态map变化时，更新当前用户的关注状态\n    LaunchedEffect(followStateMap, videoDetailViewModel.videoDetail?.author?.mid) {\n        videoDetailViewModel.videoDetail?.author?.mid?.let { mid ->\n            FollowStateManager.getFollowState(mid)?.let { following ->\n                isFollowing = following\n            }\n        }\n    }\n\n    // 添加用于管理简介对话框的状态\n    var showDescriptionDialog by remember { mutableStateOf(false) }\n\n    // 添加用于管理评论浮层的状态\n    var showCommentPanel by remember { mutableStateOf(false) }\n    val commentButtonFocusRequester = remember { FocusRequester() }\n\n    var lastPlayedCid by remember { mutableLongStateOf(0) }\n    var lastPlayedTime by remember { mutableIntStateOf(0) }\n\n    var tip by remember { mutableStateOf(\"Loading\") }\n    var showUGCVideoInfo by remember { mutableStateOf(Prefs.showUGCVideoInfo) }\n    var fromSeason by remember { mutableStateOf(false) }\n    var fromPlayer by remember { mutableStateOf(false) }\n    var paused by remember { mutableStateOf(false) }\n    var proxyArea by remember { mutableStateOf(ProxyArea.MainLand) }\n    var intentAid by remember { mutableLongStateOf(0L) }\n\n    val containsVerticalScreenVideo by remember {\n        derivedStateOf {\n            videoDetailViewModel.videoDetail?.pages?.any { it.dimension.isVertical } ?: false\n        }\n    }\n\n    var favorited by remember { mutableStateOf(false) }\n    // local copies for like / coin sync\n    var liked by remember { mutableStateOf(false) }\n    var isCoin by remember { mutableStateOf(false) }\n    val videoInFavoriteFolderIds = remember { mutableStateListOf<Long>() }\n\n    // subscribe shared action state by aid\n    val aid = videoDetailViewModel.videoDetail?.aid ?: 0L\n    val sharedActionStateFlow = remember(aid) { getStateFlow(aid, Prefs.uid) }\n    val sharedState by sharedActionStateFlow.collectAsState()\n\n    // keep local copies in sync with shared state\n    LaunchedEffect(sharedState) {\n        // sync favorite/like/coin from shared manager\n        favorited = sharedState.favorited\n        liked = sharedState.liked\n        isCoin = sharedState.coin\n        videoInFavoriteFolderIds.swapList(sharedState.favoriteFolderIds)\n    }\n\n    val setHistory = {\n        logger.info { \"play history: ${videoDetailViewModel.videoDetail?.history}\" }\n        lastPlayedCid = videoDetailViewModel.videoDetail?.history?.lastPlayedCid ?: 0\n        lastPlayedTime = videoDetailViewModel.videoDetail?.history?.progress ?: 0\n    }\n\n    val updateHistory = {\n        screenScope.launch(Dispatchers.IO) {\n            runCatching {\n                videoDetailViewModel.loadDetailOnlyUpdateHistory(videoDetailViewModel.videoDetail!!.aid)\n            }\n            withContext(Dispatchers.Main) {\n                setHistory()\n            }\n        }\n    }\n\n\n    val updateFollowingState: () -> Unit = {\n        screenScope.launch(Dispatchers.IO) {\n            val userMid = videoDetailViewModel.videoDetail?.author?.mid ?: -1\n            logger.fInfo { \"Checking is following user $userMid\" }\n            val result = FollowStateManager.ensureFollowState(userMid)\n            logger.fInfo { \"Following user result: $result\" }\n            withContext(Dispatchers.Main) {\n                showFollowButton = result != null\n                if (result != null) {\n                    isFollowing = result\n                }\n            }\n        }\n    }\n\n    val addFollow: (afterModify: (success: Boolean) -> Unit) -> Unit = { afterModify ->\n        screenScope.launch(Dispatchers.IO) {\n            val userMid = videoDetailViewModel.videoDetail?.author?.mid ?: -1\n            logger.fInfo { \"Add follow to user $userMid\" }\n            val success = userRepository.followUser(\n                mid = userMid,\n                preferApiType = Prefs.apiType\n            )\n            logger.fInfo { \"Add follow result: $success\" }\n            // 更新缓存状态\n            if (success) {\n                FollowStateManager.updateFollowState(userMid, true)\n            }\n            afterModify(success)\n        }\n    }\n\n    val delFollow: (afterModify: (success: Boolean) -> Unit) -> Unit = { afterModify ->\n        screenScope.launch(Dispatchers.IO) {\n            val userMid = videoDetailViewModel.videoDetail?.author?.mid ?: -1\n            logger.fInfo { \"Del follow to user $userMid\" }\n            val success = userRepository.unfollowUser(\n                mid = userMid,\n                preferApiType = Prefs.apiType\n            )\n            logger.fInfo { \"Del follow result: $success\" }\n            // 更新缓存状态\n            if (success) {\n                FollowStateManager.updateFollowState(userMid, false)\n            }\n            afterModify(success)\n        }\n    }\n\n    val updateVideoFavoriteData: (List<Long>) -> Unit = { folderIds ->\n        screenScope.launch {\n            val success =\n                VideoUserActionManager.updateVideoFavoriteFolders(aid, folderIds, Prefs.uid)\n            if (!success) {\n                \"收藏操作失败！此收藏夹收藏数量已达上限（1000）\".toast(context)\n            }\n        }\n    }\n\n    val addVideoToDefaultFavoriteFolder: () -> Unit = {\n        screenScope.launch {\n            val success = VideoUserActionManager.addToDefaultFavoriteFolder(aid, Prefs.uid)\n            if (!success) {\n                \"添加收藏失败！默认收藏夹不存在？\".toast(context)\n            }\n        }\n    }\n\n    val updateVideoUserActionData: suspend () -> Unit = {\n        // update shared state from loaded data\n        val configAid = videoDetailViewModel.videoDetail?.aid ?: 0L\n        VideoUserActionManager.updateFromLoadedData(\n            configAid,\n            liked = videoDetailViewModel.videoDetail?.userActions?.like ?: false,\n            favorited = videoDetailViewModel.videoDetail?.userActions?.favorite ?: false,\n            coin = videoDetailViewModel.videoDetail?.userActions?.coin ?: false\n        )\n    }\n\n    val updateUgcSeasonSectionVideoList: (Int) -> Unit = { sectionIndex ->\n        val partVideoList = mutableListOf<VideoListItem>()\n        val sectionTitle =\n            videoDetailViewModel.videoDetail!!.ugcSeason!!.sections[sectionIndex]?.title ?: \"\"\n        videoDetailViewModel.videoDetail!!.ugcSeason!!.sections[sectionIndex].episodes.mapIndexed { epIndex, episode ->\n            if (episode.pages.size == 1) {\n                episode.pages.mapIndexed { pageInd, videoPage ->\n                    partVideoList.add(\n                        VideoListUgcEpisode(\n                            aid = episode.aid,\n                            cid = videoPage.cid,\n                            title = if (sectionTitle == \"正片\") episode.title else sectionTitle,\n                            partTitle = if (sectionTitle == \"正片\") \"\" else episode.title,\n                            index = epIndex,\n                            cover = episode.cover,\n                            duration = episode.duration,\n                            pubDate = episode.pubDate,\n                        )\n                    )\n                }\n            } else {\n                partVideoList.add(\n                    VideoListUgcEpisodeTitle(\n                        title = episode.title,\n                        index = epIndex,\n                    )\n                )\n                episode.pages.mapIndexed { pageIndex, videoPage ->\n                    partVideoList.add(\n                        VideoListPart(\n                            aid = episode.aid,\n                            cid = videoPage.cid,\n                            title = episode.title,\n                            partTitle = videoPage.title,\n                            index = pageIndex,\n                            cover = episode.cover,\n                            duration = videoPage.duration,\n                            pubDate = episode.pubDate,\n                        )\n                    )\n                }\n            }\n        }\n        videoInfoRepository.videoList.clear()\n        videoInfoRepository.videoList.addAll(partVideoList)\n    }\n\n\n    suspend fun addVideoLike(): Boolean {\n        val configAid = videoDetailViewModel.videoDetail?.aid ?: 0L\n        return VideoUserActionManager.addLike(configAid, Prefs.uid)\n    }\n\n    suspend fun delVideoLike(): Boolean {\n        val configAid = videoDetailViewModel.videoDetail?.aid ?: 0L\n        return VideoUserActionManager.delLike(configAid, Prefs.uid)\n    }\n\n\n    suspend fun addVideoCoin(): Boolean {\n        val configAid = videoDetailViewModel.videoDetail?.aid ?: 0L\n        return VideoUserActionManager.addCoin(configAid, Prefs.uid)\n    }\n\n    LaunchedEffect(Unit) {\n        if (intent.hasExtra(\"aid\")) {\n            val aid = intent.getLongExtra(\"aid\", 170001)\n            intentAid = aid\n            var cid = intent.getLongExtra(\"cid\", 0)\n            fromSeason = intent.getBooleanExtra(\"fromSeason\", false)\n            fromPlayer = intent.getBooleanExtra(\"fromPlayer\", false)\n            proxyArea = ProxyArea.entries[intent.getIntExtra(\"proxy_area\", 0)]\n            //获取视频信息\n            screenScope.launch(Dispatchers.IO) {\n                if (proxyArea != ProxyArea.MainLand) {\n                    runCatching {\n                        val seasonId = BiliPlusHttpApi.getSeasonIdByAvid(aid)\n                        logger.info { \"Get season id from biliplus: $seasonId\" }\n                        seasonId?.let {\n                            logger.fInfo { \"Redirect to season $seasonId\" }\n                            SeasonInfoActivity.actionStart(\n                                context = context,\n                                seasonId = seasonId,\n                                proxyArea = proxyArea\n                            )\n                            context.finish()\n                        }\n                    }.onFailure {\n                        logger.fWarn { \"Redirect failed: ${it.stackTraceToString()}\" }\n                    }\n                }\n\n                runCatching {\n                    videoDetailViewModel.loadDetail(aid, fromSeason)\n                    updateVideoUserActionData()\n                    withContext(Dispatchers.Main) {\n                        setHistory()\n                    }\n\n                    videoInfoRepository.relatedVideos.clear()\n                    videoInfoRepository.description = videoDetailViewModel.videoDetail?.description ?: \"\"\n                    videoInfoRepository.tags = videoDetailViewModel.videoDetail?.tags ?: emptyList()\n                    if (!fromSeason) {\n                        if (Prefs.isLogin) updateFollowingState()\n\n                        videoInfoRepository.relatedVideos.addAll(\n                            videoDetailViewModel.relatedVideos.subList(\n                                0,\n                                videoDetailViewModel.relatedVideos.size))\n                    }\n                    // 从播放器推荐视频打开时 fromPlayer=true 并显示loading。300m后 fromPlayer改成false，此后从播放器返回详情页，正常显示详情内容\n                    //如果是从剧集跳转过来的或设置不显示视频详情，就直接播放 P1\n                    if (fromSeason || !showUGCVideoInfo || fromPlayer) {\n                        val shouldFinishAfterAutoLaunch = fromPlayer && !Prefs.videoInfoHistoryIncludeFromPlayer\n                        val playPart = videoDetailViewModel.videoDetail!!.pages.first()\n                        cid = cid.takeIf { it > 0L } ?: playPart.cid\n\n                        if (videoDetailViewModel.videoDetail!!.ugcSeason !== null) {\n                            val sectionIndex =\n                                videoDetailViewModel.videoDetail!!.ugcSeason!!.sections\n                                    .indexOfFirst { section -> section.episodes.any { it.cid == cid || it.pages.any { it.cid == cid } } }\n                            updateUgcSeasonSectionVideoList(sectionIndex)\n                        }\n\n                        // 检查Activity是否已经finish，如果已关闭则不启动播放器\n                        if (!context.isFinishing && !context.isDestroyed) {\n                            launchPlayerActivity(\n                                context = context,\n                                avid = videoDetailViewModel.videoDetail!!.aid,\n                                cid = cid,\n                                title = videoDetailViewModel.videoDetail!!.title,\n                                partTitle = videoDetailViewModel.videoDetail!!.pages.find { it.cid == cid }!!.title,\n                                played = if (cid == lastPlayedCid) lastPlayedTime * 1000 else 0,\n                                fromSeason = fromSeason,\n                                isVerticalVideo = videoDetailViewModel.videoDetail!!.pages.find { it.cid == cid }!!.dimension.isVertical,\n                                playerIconIdle = videoDetailViewModel.videoDetail!!.playerIcon?.idle\n                                    ?: \"\",\n                                playerIconMoving = videoDetailViewModel.videoDetail!!.playerIcon?.moving\n                                    ?: \"\",\n                                play = videoDetailViewModel.videoDetail!!.stat.view,\n                                danmaku = videoDetailViewModel.videoDetail!!.stat.danmaku,\n                                like = videoDetailViewModel.videoDetail!!.stat.like,\n                                coin = videoDetailViewModel.videoDetail!!.stat.coin,\n                                favorite = videoDetailViewModel.videoDetail!!.stat.favorite,\n                                upName = videoDetailViewModel.videoDetail!!.author.name,\n                                upId = videoDetailViewModel.videoDetail!!.author.mid,\n                                upFace = videoDetailViewModel.videoDetail!!.author.face,\n                                pubTime = videoDetailViewModel.videoDetail!!.publishDate.formatPubTimeString()\n                            )\n                        }\n                        if (shouldFinishAfterAutoLaunch) {\n                            context.finish()\n                        } else if (fromPlayer) {\n                            // 清除标记, 以便从播放器返回过来的可以进入详情页\n                            scope.launch {\n                                delay(1200)\n                                fromPlayer = false\n                                intent.removeExtra(\"fromPlayer\")\n                                if (!showUGCVideoInfo) {\n                                    context.finish()\n                                }\n                            }\n                        }\n                        if (!fromPlayer) {\n                            context.finish()\n                        }\n                    }\n                }.onFailure {\n                    val errorMessage = it.localizedMessage\n                    val isVideoNotFound = when (Prefs.apiType) {\n                        ApiType.Web -> errorMessage == \"啥都木有\"\n                        ApiType.App -> errorMessage == \"访问权限不足\"\n                    }\n\n                    logger.fInfo { \"Get video info failed: ${it.stackTraceToString()}\" }\n                    if (!isVideoNotFound || !Prefs.enableProxy) {\n                        withContext(Dispatchers.Main) {\n                            tip = it.localizedMessage ?: \"未知错误\"\n                        }\n                        return@onFailure\n                    }\n                    withContext(Dispatchers.Main) {\n                        videoDetailViewModel.state = VideoInfoState.Loading\n                    }\n\n                    logger.fInfo { \"Trying get video info through proxy server\" }\n                    runCatching {\n                        val seasonId = BiliPlusHttpApi.getSeasonIdByAvid(aid)\n                        logger.info { \"Get season id from biliplus: $seasonId\" }\n                        seasonId?.let {\n                            logger.fInfo { \"Redirect to season $seasonId\" }\n                            SeasonInfoActivity.actionStart(\n                                context = context,\n                                seasonId = seasonId,\n                                proxyArea = ProxyArea.HongKong\n                            )\n                            context.finish()\n                        } ?: let {\n                            withContext(Dispatchers.Main) {\n                                tip = \"视频不存在\"\n                                videoDetailViewModel.state = VideoInfoState.Error\n                            }\n                        }\n                    }.onFailure { e ->\n                        logger.fWarn { \"Redirect failed: ${e.stackTraceToString()}\" }\n                        withContext(Dispatchers.Main) {\n                            tip = e.localizedMessage ?: \"未知错误\"\n                            videoDetailViewModel.state = VideoInfoState.Error\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    LaunchedEffect(videoDetailViewModel.videoDetail) {\n        //如果是从剧集页跳转回来的，那就不需要再跳转到剧集页了\n        if (fromSeason || !showUGCVideoInfo) return@LaunchedEffect\n\n        videoDetailViewModel.videoDetail?.let {\n            if (it.redirectToEp) {\n                runCatching {\n                    logger.fInfo { \"Redirect to ep ${it.epid}\" }\n                    SeasonInfoActivity.actionStart(\n                        context = context,\n                        epId = it.epid,\n                        proxyArea = proxyArea\n                    )\n                    context.finish()\n                }.onFailure {\n                    logger.fWarn { \"Redirect failed: ${it.stackTraceToString()}\" }\n                }\n            } else {\n                logger.fInfo { \"No redirection required\" }\n                defaultFocusRequester.requestFocus(scope)\n            }\n        }\n    }\n\n    // 确保页面显示时封面获得焦点\n    LaunchedEffect(videoDetailViewModel.videoDetail, fromSeason, showUGCVideoInfo, fromPlayer) {\n        if (videoDetailViewModel.videoDetail != null &&\n            !videoDetailViewModel.videoDetail!!.redirectToEp &&\n            !fromSeason &&\n            showUGCVideoInfo &&\n            !fromPlayer\n        ) {\n            // 延迟一小段时间确保UI完全渲染\n            delay(300)\n            defaultFocusRequester.requestFocus(scope)\n        }\n    }\n\n    DisposableEffect(lifecycleOwner) {\n        val observer = LifecycleEventObserver { _, event ->\n            if (event == Lifecycle.Event.ON_PAUSE) {\n                paused = true\n            } else if (event == Lifecycle.Event.ON_RESUME) {\n                // 如果 pause==true 那可能是从播放页返回回来的，此时更新历史记录\n                if (paused) updateHistory()\n            }\n        }\n\n        lifecycleOwner.lifecycle.addObserver(observer)\n\n        onDispose {\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n\n    if (videoDetailViewModel.videoDetail == null || videoDetailViewModel.videoDetail?.redirectToEp == true || fromSeason || !showUGCVideoInfo || fromPlayer) {\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .ifElse(!showUGCVideoInfo, Modifier.background(Color.Black)),\n            contentAlignment = Alignment.Center\n        ) {\n            if (tip == \"Loading\") {\n                LoadingTip()\n            } else {\n                Text(\n                    text = tip,\n                    fontSize = 20.sp\n                )\n            }\n        }\n    } else {\n        Scaffold(\n            modifier = modifier\n        ) { innerPadding ->\n            Box(\n                Modifier.padding(innerPadding)\n            ) {\n                // 图片加载成功后，动画 alpha，从 0 -> 0.6f\n                val bgLoaded = remember { mutableStateOf(false) }\n                val animatedAlpha by animateFloatAsState(\n                    targetValue = if (bgLoaded.value) 0.6f else 0f,\n                    animationSpec = tween(durationMillis = 500)\n                )\n                AsyncImage(\n                    modifier = Modifier.fillMaxSize(),\n                    model = ImageRequest.Builder(LocalContext.current)\n                        .data(videoDetailViewModel.videoDetail?.cover)\n                        .transformations(BlurTransformation(LocalContext.current, 20f, 5f))\n                        .build(),\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop,\n                    alpha = animatedAlpha,\n                    onSuccess = { bgLoaded.value = true },\n                    onError = { bgLoaded.value = false }\n                )\n\n                LazyColumn(\n                    modifier = Modifier\n                        .onKeyEvent { event ->\n                            if (event.type == KeyEventType.KeyDown) {\n                                when (event.key) {\n                                    Key.DirectionUp -> {\n                                        scope.launch {\n                                            lazyListState.animateScrollBy(-200f)\n                                        }\n                                    }\n                                    Key.DirectionDown -> {\n                                        scope.launch {\n                                            lazyListState.animateScrollBy(200f)\n                                        }\n                                    }\n                                }\n                            }\n                            return@onKeyEvent false\n                        },\n                    state = lazyListState,\n                    contentPadding = PaddingValues(top = 16.dp, bottom = 16.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    item {\n                        Column(\n                            verticalArrangement = Arrangement.spacedBy(8.dp)\n                        ) {\n                            if (containsVerticalScreenVideo) {\n                                ArgueTip(text = stringResource(R.string.video_info_argue_tip_vertical_screen))\n                            }\n                            if (videoDetailViewModel.videoDetail?.argueTip != null) {\n                                ArgueTip(text = videoDetailViewModel.videoDetail!!.argueTip!!)\n                            }\n                        }\n                    }\n                    item {\n                        VideoInfoData(\n                            defaultFocusRequester = defaultFocusRequester,\n                            videoDetail = videoDetailViewModel.videoDetail!!,\n                            showFollowButton = showFollowButton,\n                            isFollowing = isFollowing,\n                            tags = videoDetailViewModel.videoDetail!!.tags,\n                            isFavorite = favorited,\n                            favoriteFolderIds = videoInFavoriteFolderIds,\n                            onClickCover = {\n                                logger.fInfo { \"Click video cover\" }\n                                var title = \"\"\n                                var partTitle = \"\"\n                                //set video list\n                                if (videoDetailViewModel.videoDetail?.ugcSeason != null) {\n                                    // 合集\n                                    if (videoDetailViewModel.videoDetail!!.ugcSeason!!.sections.size == 1) {\n                                        // 只有一个分组\n                                        updateUgcSeasonSectionVideoList(0)\n                                    } else {\n                                        // 多个组，找默认播放哪个组的\n                                        val cid =\n                                            videoDetailViewModel.videoDetail!!.pages.first().cid\n                                        val sectionIndex =\n                                            videoDetailViewModel.videoDetail!!.ugcSeason!!.sections\n                                                .indexOfFirst { section -> section.episodes.any { it.cid == cid || it.pages.any { it.cid == cid } } }\n                                        val section =\n                                            videoDetailViewModel.videoDetail!!.ugcSeason!!.sections.getOrNull(\n                                                sectionIndex\n                                            )\n                                        title =\n                                            if (section?.title == \"正片\") section.episodes.find { it.cid == cid }!!.title else section?.title\n                                                ?: \"\"\n                                        partTitle =\n                                            if (section?.title == \"正片\") \"\" else section?.episodes?.find { it.cid == cid }!!.title\n                                        updateUgcSeasonSectionVideoList(sectionIndex)\n                                    }\n                                } else {\n                                    // 分 p\n                                    val partVideoList =\n                                        videoDetailViewModel.videoDetail!!.pages.mapIndexed { index, videoPage ->\n                                            VideoListPart(\n                                                aid = videoDetailViewModel.videoDetail!!.aid,\n                                                cid = videoPage.cid,\n                                                title = videoDetailViewModel.videoDetail!!.title,\n                                                partTitle = if (videoDetailViewModel.videoDetail!!.pages.size == 1) \"\" else videoPage.title,\n                                                index = index,\n                                                cover = videoDetailViewModel.videoDetail!!.cover,\n                                                duration = videoPage.duration,\n                                            )\n                                        }\n                                    videoInfoRepository.videoList.clear()\n                                    videoInfoRepository.videoList.addAll(partVideoList)\n                                }\n\n                                val lastPlayedPage =\n                                    videoDetailViewModel.videoDetail!!.pages.find { it.cid == lastPlayedCid }\n                                val playPage = lastPlayedPage\n                                    ?: videoDetailViewModel.videoDetail!!.pages.first()\n\n                                launchPlayerActivity(\n                                    context = context,\n                                    avid = videoDetailViewModel.videoDetail!!.aid,\n                                    cid = playPage.cid,\n                                    title = if (title.isNotEmpty()) title else videoDetailViewModel.videoDetail!!.title,\n                                    partTitle = if (partTitle.isNotEmpty()) partTitle else if (videoDetailViewModel.videoDetail!!.pages.size == 1) \"\" else playPage.title,\n                                    played = if (playPage.cid == lastPlayedCid) lastPlayedTime * 1000 else 0,\n                                    fromSeason = false,\n                                    isVerticalVideo = videoDetailViewModel.videoDetail!!.pages.first().dimension.isVertical,\n                                    playerIconIdle = videoDetailViewModel.videoDetail!!.playerIcon?.idle\n                                        ?: \"\",\n                                    playerIconMoving = videoDetailViewModel.videoDetail!!.playerIcon?.moving\n                                        ?: \"\",\n                                    play = videoDetailViewModel.videoDetail!!.stat.view,\n                                    danmaku = videoDetailViewModel.videoDetail!!.stat.danmaku,\n                                    like = videoDetailViewModel.videoDetail!!.stat.like,\n                                    coin = videoDetailViewModel.videoDetail!!.stat.coin,\n                                    favorite = videoDetailViewModel.videoDetail!!.stat.favorite,\n                                    upName = videoDetailViewModel.videoDetail!!.author.name,\n                                    upId = videoDetailViewModel.videoDetail!!.author.mid,\n                                    upFace = videoDetailViewModel.videoDetail!!.author.face,\n                                    pubTime = videoDetailViewModel.videoDetail!!.publishDate.formatPubTimeString()\n                                )\n                            },\n                            onClickUp = {\n                                UpInfoActivity.actionStart(\n                                    context,\n                                    mid = videoDetailViewModel.videoDetail!!.author.mid,\n                                    name = videoDetailViewModel.videoDetail!!.author.name,\n                                    face = videoDetailViewModel.videoDetail!!.author.face\n                                )\n                            },\n                            onAddFollow = {\n                                addFollow { success ->\n                                    screenScope.launch(Dispatchers.Main) {\n                                        if (success) {\n                                            \"关注成功\".toast(context)\n                                        } else {\n                                            \"关注失败\".toast(context)\n                                        }\n                                    }\n                                }\n                            },\n                            onDelFollow = {\n                                delFollow { success ->\n                                    screenScope.launch(Dispatchers.Main) {\n                                        if (success) {\n                                            \"已取消关注\".toast(context)\n                                        } else {\n                                            \"取消关注失败\".toast(context)\n                                        }\n                                    }\n                                }\n                            },\n                            onClickTip = { tag ->\n                                TagActivity.actionStart(\n                                    context = context,\n                                    tagId = tag.id,\n                                    tagName = tag.name\n                                )\n                            },\n                            onAddToDefaultFavoriteFolder = {\n                                addVideoToDefaultFavoriteFolder()\n                                favorited = true\n                                \"已添加到默认收藏夹\".toast(context)\n                            },\n                            onUpdateFavoriteFolders = {\n                                updateVideoFavoriteData(it)\n                                favorited = it.isNotEmpty()\n                                videoInFavoriteFolderIds.swapList(it)\n                                if (it.isNotEmpty()) {\n                                    \"收藏成功\".toast(context)\n                                } else {\n                                    \"已取消收藏\".toast(context)\n                                }\n                            },\n                            isLike = liked,\n                            onAddLike = {\n                                screenScope.launch {\n                                    if (!liked) {\n                                        if (addVideoLike()) {\n                                            liked = true\n                                            \"点赞成功\".toast(context)\n                                        } else {\n                                            \"点赞失败\".toast(context)\n                                        }\n                                    }\n                                }\n                            },\n                            onDelLike = {\n                                screenScope.launch {\n                                    if (liked) {\n                                        if (delVideoLike()) {\n                                            liked = false\n                                            \"已取消点赞\".toast(context)\n                                        } else {\n                                            \"取消点赞失败\".toast(context)\n                                        }\n                                    }\n                                }\n                            },\n                            isCoin = isCoin,\n                            onAddCoin = {\n                                screenScope.launch {\n                                    if (!isCoin) {\n                                        if (addVideoCoin()) {\n                                            isCoin = true\n                                            \"投币成功\".toast(context)\n                                        } else {\n                                            \"投币失败\".toast(context)\n                                        }\n                                    }\n                                }\n                            },\n                            onShowDescription = {\n                                showDescriptionDialog = true\n                            },\n                            onShowComment = {\n                                showCommentPanel = true\n                            },\n                            commentButtonFocusRequester = commentButtonFocusRequester\n                        )\n                    }\n                    if (videoDetailViewModel.videoDetail?.ugcSeason == null) {\n                        item {\n                            VideoPartRow(\n                                pages = videoDetailViewModel.videoDetail?.pages ?: emptyList(),\n                                lastPlayedCid = lastPlayedCid,\n                                lastPlayedTime = lastPlayedTime,\n                                enablePartListDialog =\n                                    (videoDetailViewModel.videoDetail?.pages?.size ?: 0) > 5,\n                                onClick = { cid ->\n                                    logger.fInfo { \"Click video part: [av:${videoDetailViewModel.videoDetail?.aid}, bv:${videoDetailViewModel.videoDetail?.bvid}, cid:$cid]\" }\n                                    launchPlayerActivity(\n                                        context = context,\n                                        avid = videoDetailViewModel.videoDetail!!.aid,\n                                        cid = cid,\n                                        title = videoDetailViewModel.videoDetail!!.title,\n                                        partTitle = videoDetailViewModel.videoDetail!!.pages.find { it.cid == cid }!!.title,\n                                        played = if (cid == lastPlayedCid) lastPlayedTime * 1000 else 0,\n                                        fromSeason = false,\n                                        isVerticalVideo = videoDetailViewModel.videoDetail!!.pages.find { it.cid == cid }!!.dimension.isVertical,\n                                        playerIconIdle = videoDetailViewModel.videoDetail!!.playerIcon?.idle\n                                            ?: \"\",\n                                        playerIconMoving = videoDetailViewModel.videoDetail!!.playerIcon?.moving\n                                            ?: \"\",\n                                        play = videoDetailViewModel.videoDetail!!.stat.view,\n                                        danmaku = videoDetailViewModel.videoDetail!!.stat.danmaku,\n                                        like = videoDetailViewModel.videoDetail!!.stat.like,\n                                        coin = videoDetailViewModel.videoDetail!!.stat.coin,\n                                        favorite = videoDetailViewModel.videoDetail!!.stat.favorite,\n                                        upName = videoDetailViewModel.videoDetail!!.author.name,\n                                        upId = videoDetailViewModel.videoDetail!!.author.mid,\n                                        upFace = videoDetailViewModel.videoDetail!!.author.face,\n                                        pubTime = videoDetailViewModel.videoDetail!!.publishDate.formatPubTimeString()\n                                    )\n                                }\n                            )\n                        }\n                    } else {\n                        itemsIndexed(\n                            items = videoDetailViewModel.videoDetail?.ugcSeason!!.sections,\n                            key = { index, section -> \"$index-section-${section.title}-${section.episodes.firstOrNull()?.aid ?: index}-${section.episodes.firstOrNull()?.cid ?: 0}\" }\n                        ) { index, section ->\n                            VideoUgcSeasonRow(\n                                title = section.title,\n                                episodes = section.episodes,\n                                lastPlayedCid = lastPlayedCid,\n                                lastPlayedTime = lastPlayedTime,\n                                intentAid = intentAid,\n                                enableUgcListDialog = section.episodes.size > 5,\n                                onClickEp = { aid, cid ->\n                                    logger.fInfo { \"Click ugc season episode: [av:${videoDetailViewModel.videoDetail?.aid}, bv:${videoDetailViewModel.videoDetail?.bvid}, cid:$cid]\" }\n                                    updateUgcSeasonSectionVideoList(index)\n                                    val sectionTitle =\n                                        videoDetailViewModel.videoDetail?.ugcSeason?.sections?.getOrNull(\n                                            index\n                                        )?.title\n                                    val episode = section.episodes.find { it.cid == cid }\n                                    launchPlayerActivity(\n                                        context = context,\n                                        avid = aid,\n                                        cid = cid,\n                                        title = if (sectionTitle == \"正片\") episode!!.title else sectionTitle\n                                            ?: videoDetailViewModel.videoDetail?.ugcSeason?.title\n                                            ?: \"\",\n                                        partTitle = if (sectionTitle == \"正片\") if (episode!!.pages.size > 1) episode.pages.first().title else \"\" else episode!!.title,\n                                        played = if (cid == lastPlayedCid) lastPlayedTime * 1000 else 0,\n                                        fromSeason = false,\n                                        isVerticalVideo = if (sectionTitle == \"正片\" && episode!!.pages.size > 1) episode.pages.first().dimension.isVertical else videoDetailViewModel.videoDetail!!.pages.first().dimension.isVertical,\n                                        playerIconIdle = videoDetailViewModel.videoDetail!!.playerIcon?.idle\n                                            ?: \"\",\n                                        playerIconMoving = videoDetailViewModel.videoDetail!!.playerIcon?.moving\n                                            ?: \"\",\n                                        play = videoDetailViewModel.videoDetail!!.stat.view,\n                                        danmaku = videoDetailViewModel.videoDetail!!.stat.danmaku,\n                                        like = videoDetailViewModel.videoDetail!!.stat.like,\n                                        coin = videoDetailViewModel.videoDetail!!.stat.coin,\n                                        favorite = videoDetailViewModel.videoDetail!!.stat.favorite,\n                                        upName = videoDetailViewModel.videoDetail!!.author.name,\n                                        upId = videoDetailViewModel.videoDetail!!.author.mid,\n                                        upFace = videoDetailViewModel.videoDetail!!.author.face,\n                                        pubTime = videoDetailViewModel.videoDetail!!.publishDate.formatPubTimeString()\n                                    )\n                                },\n                                onClickEpPart = { episode, cid ->\n                                    logger.fInfo { \"Click ugc season episode part: [av:${videoDetailViewModel.videoDetail?.aid}, bv:${videoDetailViewModel.videoDetail?.bvid}, cid:$cid]\" }\n                                    val sectionTitle =\n                                        videoDetailViewModel.videoDetail?.ugcSeason?.sections?.getOrNull(\n                                            index\n                                        )?.title\n                                    launchPlayerActivity(\n                                        context = context,\n                                        avid = episode.aid,\n                                        cid = cid,\n                                        title = if (!sectionTitle.isNullOrEmpty()) episode.title else videoDetailViewModel.videoDetail!!.title,\n                                        partTitle = episode.pages.find { it.cid == cid }!!.title,\n                                        played = if (cid == lastPlayedCid) lastPlayedTime * 1000 else 0,\n                                        fromSeason = false,\n                                        isVerticalVideo = if (!sectionTitle.isNullOrEmpty()) episode.pages.find { it.cid == cid }!!.dimension.isVertical else videoDetailViewModel.videoDetail!!.pages.find { it.cid == cid }!!.dimension.isVertical,\n                                        playerIconIdle = videoDetailViewModel.videoDetail!!.playerIcon?.idle\n                                            ?: \"\",\n                                        playerIconMoving = videoDetailViewModel.videoDetail!!.playerIcon?.moving\n                                            ?: \"\",\n                                        play = videoDetailViewModel.videoDetail!!.stat.view,\n                                        danmaku = videoDetailViewModel.videoDetail!!.stat.danmaku,\n                                        like = videoDetailViewModel.videoDetail!!.stat.like,\n                                        coin = videoDetailViewModel.videoDetail!!.stat.coin,\n                                        favorite = videoDetailViewModel.videoDetail!!.stat.favorite,\n                                        upName = videoDetailViewModel.videoDetail!!.author.name,\n                                        upId = videoDetailViewModel.videoDetail!!.author.mid,\n                                        upFace = videoDetailViewModel.videoDetail!!.author.face,\n                                        pubTime = videoDetailViewModel.videoDetail!!.publishDate.formatPubTimeString()\n                                    )\n                                }\n                            )\n                        }\n                    }\n                    if (videoDetailViewModel.relatedVideos.isNotEmpty()) {\n                        item {\n                            VideosRow(\n                                header = stringResource(R.string.video_info_related_video_title),\n                                videos = videoDetailViewModel.relatedVideos,\n                                showMore = {},\n                                onOpenSeasonInfo = { videoData ->\n                                    SeasonInfoActivity.actionStart(\n                                        context = context,\n                                        epId = videoData.epId!!,\n                                        proxyArea = ProxyArea.checkProxyArea(videoData.title)\n                                    )\n                                },\n                                onOpenVideoInfo = { videoData ->\n                                    VideoInfoActivity.actionStart(context, videoData.avid)\n                                }\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    VideoDescriptionDialog(\n        show = showDescriptionDialog,\n        onHideDialog = { showDescriptionDialog = false },\n        description = videoDetailViewModel.videoDetail?.description ?: \"\"\n    )\n\n    // 计算评论面板的初始 episode id（用于 UGC 合集）\n    val commentInitialEpisodeId = remember(lastPlayedCid, intentAid, videoDetailViewModel.videoDetail?.ugcSeason) {\n        val sections = videoDetailViewModel.videoDetail?.ugcSeason?.sections ?: return@remember -1\n        val allEpisodes = sections.flatMap { it.episodes }\n\n        // 优先使用历史记录对应的 episode\n        if (lastPlayedCid != 0L) {\n            allEpisodes.find { ep ->\n                ep.cid == lastPlayedCid || ep.pages.any { it.cid == lastPlayedCid }\n            }?.id?.let { return@remember it }\n        }\n\n        // 没有历史记录时，使用与 intentAid 匹配的 episode\n        if (intentAid != 0L) {\n            allEpisodes.find { it.aid == intentAid }?.id?.let { return@remember it }\n        }\n\n        -1\n    }\n\n    CommentPanel(\n        show = showCommentPanel,\n        oid = videoDetailViewModel.videoDetail?.aid ?: 0L,\n        onHide = { showCommentPanel = false },\n        sections = videoDetailViewModel.videoDetail?.ugcSeason?.sections ?: emptyList(),\n        initialEpisodeId = commentInitialEpisodeId\n    )\n\n    // 浮层关闭后，焦点返回评论按钮\n    LaunchedEffect(showCommentPanel) {\n        if (!showCommentPanel) {\n            commentButtonFocusRequester.requestFocus()\n        }\n    }\n}\n\n@Composable\nfun ArgueTip(\n    modifier: Modifier = Modifier,\n    text: String\n) {\n    Surface(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(horizontal = 36.dp),\n        colors = SurfaceDefaults.colors(\n            containerColor = Color.Yellow.copy(alpha = 0.2f),\n            contentColor = Color.Yellow\n        ),\n        shape = MaterialTheme.shapes.small\n    ) {\n        Row(\n            modifier = Modifier.padding(\n                horizontal = 16.dp,\n                vertical = 8.dp\n            ),\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Icon(\n                imageVector = Icons.Rounded.Warning,\n                contentDescription = null,\n                tint = Color.Yellow\n            )\n            Text(text = text)\n        }\n    }\n}\n\n@OptIn(ExperimentalTvMaterial3Api::class)\n@Composable\nfun VideoInfoData(\n    modifier: Modifier = Modifier,\n    defaultFocusRequester: FocusRequester,\n    videoDetail: VideoDetail,\n    showFollowButton: Boolean,\n    isFollowing: Boolean,\n    tags: List<Tag>,\n    isFavorite: Boolean,\n    favoriteFolderIds: List<Long> = emptyList(),\n    onClickCover: () -> Unit,\n    onClickUp: () -> Unit,\n    onAddFollow: () -> Unit,\n    onDelFollow: () -> Unit,\n    onClickTip: (Tag) -> Unit,\n    onAddToDefaultFavoriteFolder: () -> Unit,\n    onUpdateFavoriteFolders: (List<Long>) -> Unit,\n    isLike: Boolean,\n    onAddLike: () -> Unit = {},\n    onDelLike: () -> Unit = {},\n    isCoin: Boolean = false,\n    onAddCoin: () -> Unit = {},\n    onShowDescription: () -> Unit = {},\n    onShowComment: () -> Unit = {},\n    commentButtonFocusRequester: FocusRequester\n) {\n//    val localDensity = LocalDensity.current\n//    var heightIs by remember { mutableStateOf(0.dp) }\n    val isLogin by remember { mutableStateOf(Prefs.isLogin) }\n//    var coverHasFocus by remember { mutableStateOf(false) }\n    val videoDuration =\n        videoDetail.pages.sumOf { it.duration }.takeIf { videoDetail.pages.isNotEmpty() } ?: 0\n\n    Row(\n        modifier = modifier\n            .padding(start = 36.dp, end = 36.dp, top = 12.dp, bottom = 18.dp),\n    ) {\n        Surface(\n            modifier = Modifier\n                .focusRequester(defaultFocusRequester)\n                .width(260.dp)\n                .aspectRatio(1.6f)\n//                .onGloballyPositioned { coordinates ->\n//                    heightIs = with(localDensity) { coordinates.size.height.toDp() }\n//                }\n//                .onFocusChanged { coverHasFocus = it.hasFocus }\n                .padding(4.dp),\n            onClick = onClickCover,\n            shape = ClickableSurfaceDefaults.shape(\n                shape = MaterialTheme.shapes.medium,\n            ),\n            scale = ClickableSurfaceDefaults.scale(scale = 1f, focusedScale = 1.05f),\n            border = ClickableSurfaceDefaults.border(\n                focusedBorder = Border(\n                    border = BorderStroke(\n                        width = 3.dp,\n                        color = MaterialTheme.colorScheme.border\n                    ),\n                    shape = MaterialTheme.shapes.medium\n                )\n            )\n        ) {\n            AsyncImage(\n                modifier = Modifier\n                    .fillMaxSize(),\n                // model = if (videoDetail.ugcSeason != null) videoDetail.ugcSeason!!.cover else videoDetail.cover,\n                model = videoDetail.cover,\n                contentDescription = null,\n                contentScale = ContentScale.Crop\n            )\n            if (videoDetail.isChargingArc) {\n                Text(\n                    modifier = Modifier\n                        .padding(6.dp)\n                        .align(Alignment.TopEnd)\n                        .background(\n                            color = Color.Black.copy(0.3f),\n                            shape = MaterialTheme.shapes.extraSmall\n                        )\n                        .padding(all = 2.dp),\n                    text = \"⚡${videoDetail.chargingArcBadge}\",\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = Color.White\n                )\n            }\n            Box(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .fillMaxWidth()\n                    .height(48.dp)\n                    .clip(\n                        RoundedCornerShape(\n                            topStart = 0.dp,\n                            topEnd = 0.dp,\n                            bottomStart = 12.dp,\n                            bottomEnd = 12.dp\n                        )\n                    )\n                    .background(\n                        Brush.verticalGradient(\n                            colors = listOf(\n                                Color.Transparent,\n                                Color.Black.copy(alpha = 0.8f)\n                            )\n                        )\n                    )\n            ) {\n                Row(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .padding(start = 16.dp, end = 16.dp, bottom = 12.dp),\n                    verticalAlignment = Alignment.Bottom,\n                    horizontalArrangement = Arrangement.spacedBy(2.dp)\n                ) {\n                    Icon(\n                        modifier = Modifier,\n                        painter = painterResource(id = R.drawable.ic_play_count),\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                    Text(\n                        text = with(videoDetail.stat.view) { if (this >= 10000) \"${this / 10000}万\" else \"$this\" },\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White\n                    )\n                    Spacer(modifier = Modifier.width(6.dp))\n                    Icon(\n                        modifier = Modifier,\n                        painter = painterResource(id = R.drawable.ic_danmaku_count),\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                    Text(\n                        text = with(videoDetail.stat.danmaku) { if (this >= 10000) \"${this / 10000}万\" else \"$this\" },\n                        style = MaterialTheme.typography.bodySmall,\n                        color = Color.White\n                    )\n                    Spacer(Modifier.weight(1f))\n                    Text(\n                        text = (videoDuration * 1000L).formatHourMinSec(),\n                        color = Color.White,\n                        style = MaterialTheme.typography.bodySmall\n                    )\n                }\n            }\n        }\n        Spacer(modifier = Modifier.width(24.dp))\n        Column(\n            modifier = Modifier\n                .fillMaxWidth(),\n//                .height(heightIs),\n            verticalArrangement = Arrangement.spacedBy(20.dp)\n        ) {\n            // 基本信息\n            Column(\n                verticalArrangement = Arrangement.spacedBy(4.dp)\n            ) {\n                Text(\n                    text = videoDetail.title,\n                    style = MaterialTheme.typography.titleLarge,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    color = Color.White\n                )\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 4.dp, bottom = 10.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(2.dp)\n                ) {\n                    CompositionLocalProvider(\n                        LocalTextStyle provides MaterialTheme.typography.labelMedium\n                    ) {\n                        Text(text = \"${videoDetail.stat.like} 点赞\")\n                        Text(text = \"·\")\n                        Text(text = \"${videoDetail.stat.coin} 投币\")\n                        Text(text = \"·\")\n                        Text(text = \"${videoDetail.stat.favorite} 收藏\")\n                        Text(text = \"·\")\n                        Text(text = videoDetail.publishDate.formatPubTimeString())\n                    }\n                }\n                LazyRow(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .offset(x = (-3).dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(4.dp),\n                    contentPadding = PaddingValues(horizontal = 4.dp)\n                ) {\n                    if (isLogin) {\n                        item {\n                            LikeButton(\n                                modifier = Modifier\n                                    .height(32.dp), // 设置高度\n                                isLike = isLike,\n                                onToggleLike = {\n                                    if (isLike) {\n                                        onDelLike()\n                                    } else {\n                                        onAddLike()\n                                    }\n                                }\n                            )\n                        }\n                        item {\n                            FavoriteButton(\n                                modifier = Modifier\n                                    .height(32.dp), // 设置高度\n                                isFavorite = isFavorite,\n                                favoriteFolderIds = favoriteFolderIds,\n                                onAddToDefaultFavoriteFolder = onAddToDefaultFavoriteFolder,\n                                onUpdateFavoriteFolders = onUpdateFavoriteFolders\n                            )\n                        }\n                        item {\n                            CoinButton(\n                                modifier = Modifier\n                                    .height(32.dp), // 设置高度\n                                isCoin = isCoin,\n                                onAddCoin = {\n                                    onAddCoin()\n                                }\n                            )\n                        }\n                    }\n                    item {\n                        UpButton(\n                            name = videoDetail.author.name,\n                            followed = isFollowing,\n                            showFollowButton = showFollowButton,\n                            onClickUp = onClickUp,\n                            onAddFollow = onAddFollow,\n                            onDelFollow = onDelFollow\n                        )\n                    }\n\n                    // 简介按钮\n                    if (videoDetail.description.isNotBlank()) {\n                        item {\n                            Row(\n                                modifier = Modifier\n                                    .clip(MaterialTheme.shapes.small)\n                                    .background(Color.White.copy(alpha = 0.2f))\n                                    .focusedBorder(MaterialTheme.shapes.small)\n                                    .padding(horizontal = 4.dp)\n                                    .clickable { onShowDescription() }\n                                    .height(30.dp),\n                                verticalAlignment = Alignment.CenterVertically,\n                            ) {\n                                Text(\n                                    text = \"简介>>\",\n                                    color = Color.White\n                                )\n                            }\n                        }\n                    }\n\n                    // 评论按钮\n                    item {\n                        Row(\n                            modifier = Modifier\n                                .clip(MaterialTheme.shapes.small)\n                                .background(Color.White.copy(alpha = 0.2f))\n                                .focusedBorder(MaterialTheme.shapes.small)\n                                .padding(horizontal = 4.dp)\n                                .focusRequester(commentButtonFocusRequester)\n                                .clickable { onShowComment() }\n                                .height(30.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                        ) {\n                            Text(\n                                text = \"评论>>\",\n                                color = Color.White\n                            )\n                        }\n                    }\n                }\n            }\n            // 标签列表\n            LazyRow(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .offset(x = (-2).dp, y = (-2).dp),\n                contentPadding = PaddingValues(horizontal = 4.dp),\n                horizontalArrangement = Arrangement.spacedBy(6.dp)\n            ) {\n                itemsIndexed(\n                    items = tags,\n                    key = { index, tag -> \"$index-tag-${tag.name}\" }\n                ) { _, tag ->\n                    SuggestionChip(onClick = {\n                        onClickTip(tag)\n                    }) {\n                        Text(text = tag.name)\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun UpButton(\n    modifier: Modifier = Modifier,\n    name: String,\n    followed: Boolean,\n    showFollowButton: Boolean = false,\n    onClickUp: () -> Unit,\n    onAddFollow: () -> Unit,\n    onDelFollow: () -> Unit\n) {\n    val view = LocalView.current\n    val isLogin by remember { mutableStateOf(if (!view.isInEditMode) Prefs.isLogin else true) }\n\n    Row(\n        modifier = modifier,\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.spacedBy(4.dp)\n    ) {\n        Row(\n            modifier = Modifier\n                .clip(MaterialTheme.shapes.small)\n                .background(Color.White.copy(alpha = 0.2f))\n                .focusedBorder(MaterialTheme.shapes.small)\n                .padding(4.dp)\n                .clickable { onClickUp() },\n            horizontalArrangement = Arrangement.spacedBy(4.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            UpIcon(color = Color.White)\n            Text(text = name, color = Color.White)\n        }\n        if (isLogin && showFollowButton) {\n            Row(\n                modifier = Modifier\n                    .clip(MaterialTheme.shapes.small)\n                    .background(Color.White.copy(alpha = 0.2f))\n                    .focusedBorder(MaterialTheme.shapes.small)\n                    .padding(horizontal = 4.dp, vertical = 3.dp)\n                    .clickable { if (followed) onDelFollow() else onAddFollow() }\n                    .animateContentSize(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                if (followed) {\n                    Icon(\n                        imageVector = Icons.Rounded.Done,\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                    Text(\n                        text = stringResource(R.string.video_info_followed),\n                        color = Color.White\n                    )\n                } else {\n                    Icon(\n                        imageVector = Icons.Rounded.Add,\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                    Text(text = stringResource(R.string.video_info_follow), color = Color.White)\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun VideoDescriptionDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    description: String\n) {\n    val state = rememberLazyListState()\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(show) {\n        if (show) {\n            focusRequester.requestFocus()\n        }\n    }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier\n                .fillMaxWidth(0.8f),\n            onDismissRequest = { onHideDialog() },\n            properties = DialogProperties(usePlatformDefaultWidth = false),\n            title = {\n                Text(\n                    text = stringResource(R.string.video_info_description_title),\n                    color = Color.White\n                )\n            },\n            text = {\n                LazyColumn(\n                    modifier = Modifier\n                        .heightIn(max = 320.dp)\n                        .focusable()\n                        .focusRequester(focusRequester)\n                        .onKeyEvent { event ->\n                            if (event.type == KeyEventType.KeyDown) {\n                                when (event.key) {\n                                    Key.DirectionUp -> {\n                                        scope.launch { state.animateScrollBy(-state.layoutInfo.viewportSize.height / 3f) }\n                                        true\n                                    }\n\n                                    Key.DirectionDown -> {\n                                        scope.launch { state.animateScrollBy(state.layoutInfo.viewportSize.height / 3f) }\n                                        true\n                                    }\n\n                                    else -> false\n                                }\n                            } else {\n                                false\n                            }\n                        },\n                    state = state\n                ) {\n                    item {\n                        Text(text = description)\n                    }\n                }\n            },\n            confirmButton = {}\n        )\n    }\n}\n\n@Composable\nprivate fun VideoPartButton(\n    modifier: Modifier = Modifier,\n    index: Int,\n    title: String,\n    duration: Int,\n    cover: String? = null,\n    pubDate: Long = 0L,\n    played: Int = 0,\n    isLastPlayed: Boolean = false,\n    isCurrentIntent: Boolean = false,\n    type: VideoPartType = VideoPartType.Part,\n    onClick: () -> Unit\n) {\n    var hasFocus by remember { mutableStateOf(false) }\n    val hasCover = !cover.isNullOrBlank()\n\n    Surface(\n        modifier = modifier.onFocusChanged { hasFocus = it.hasFocus },\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.9f),\n            focusedContainerColor = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.9f),\n            focusedContentColor = MaterialTheme.colorScheme.background\n        ),\n        scale = ClickableSurfaceDefaults.scale(scale = 1f, focusedScale = 1f),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        onClick = { onClick() }\n    ) {\n        val borderStroke = when {\n            hasFocus -> BorderStroke(2.dp, MaterialTheme.colorScheme.border)\n            isCurrentIntent -> BorderStroke(2.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f))\n            else -> null\n        }\n        Box(\n            modifier = Modifier\n                .height(72.dp)\n                .width(if (hasCover) 240.dp else 200.dp)\n                .then(\n                    if (borderStroke != null) Modifier.border(\n                        border = borderStroke,\n                        shape = MaterialTheme.shapes.medium\n                    ) else Modifier\n                )\n        ) {\n            Row(\n                modifier = Modifier\n                    .fillMaxSize()\n            ) {\n                if (hasCover) {\n                    AsyncImage(\n                        model = cover.resizedImageUrl(ImageSize.UgcEpisodeCover),\n                        contentDescription = null,\n                        contentScale = ContentScale.Crop,\n                        modifier = Modifier\n                            .fillMaxHeight()\n                            .aspectRatio(4f / 3f)\n                            .clip(RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp))\n                    )\n                }\n                Column(\n                    modifier = Modifier\n                        .fillMaxHeight()\n                        .weight(1f)\n                        .padding(horizontal = 8.dp, vertical = 6.dp),\n                    verticalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Text(\n                        text = buildAnnotatedString {\n                            if (isLastPlayed) {\n                                withStyle(style = SpanStyle(color = Color(0xFFE39B17))) {\n                                    append(\"继续播放 \")\n                                }\n                            }\n                            append(when (type) {\n                                VideoPartType.Episode -> \"EP\"\n                                VideoPartType.Part -> \"P\"\n                            } + \"$index $title\")\n                        },\n                        maxLines = 2,\n                        overflow = TextOverflow.Ellipsis,\n                        style = MaterialTheme.typography.bodyMedium.copy(\n                            lineHeight = 19.sp\n                        )\n                    )\n                    Row(\n                        modifier = Modifier.padding(top = 2.dp),\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Text(\n                            text = (duration * 1000L).formatHourMinSec(),\n                            fontSize = 11.sp,\n                            color = LocalContentColor.current.copy(alpha = 0.9f),\n                            maxLines = 1\n                        )\n                        if (pubDate > 0L) {\n                            Text(\n                                text = Date(pubDate * 1000L).formatPubTimeString(),\n                                fontSize = 11.sp,\n                                color = LocalContentColor.current.copy(alpha = 0.9f),\n                                maxLines = 1\n                            )\n                        }\n                    }\n                }\n            }\n            if (played != 0) {\n                val progressFraction = if (played < 0) 1f else (played / duration.toFloat()).coerceIn(0f, 1f)\n                val sliderColors = SliderDefaults.colors()\n                Box(\n                    modifier = Modifier\n                        .align(Alignment.BottomStart)\n                        .height(5.dp)\n                        .fillMaxWidth(progressFraction)\n                        .background(Color(0xFFE39B17))\n                ) {}\n            }\n        }\n    }\n}\n\nprivate enum class VideoPartType {\n    Episode, Part\n}\n\n@Composable\nprivate fun VideoPartRowButton(\n    modifier: Modifier = Modifier,\n    hasFocus: Boolean = true,\n    onClick: () -> Unit\n) {\n    val scale by animateFloatAsState(\n        targetValue = if (hasFocus) 1f else 0.4f,\n        label = \"button scale\",\n        animationSpec = tween(\n            durationMillis = 120\n        )\n    )\n\n    Surface(\n        modifier = modifier,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surfaceVariant,\n            focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n            pressedContainerColor = MaterialTheme.colorScheme.inverseSurface\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.small),\n        border = ClickableSurfaceDefaults.border(\n            focusedBorder = Border(\n                border = BorderStroke(2.dp, MaterialTheme.colorScheme.border),\n                shape = MaterialTheme.shapes.small\n            )\n        ),\n        onClick = onClick\n    ) {\n        Box(\n            modifier = Modifier\n                .size(width = (40 * scale).dp, height = (42 * scale).dp),\n            contentAlignment = Alignment.Center\n        ) {\n            Icon(\n                modifier = Modifier\n                    .size(32.dp)\n                    .rotate(90f),\n                imageVector = Icons.Rounded.ViewModule,\n                contentDescription = null\n            )\n        }\n    }\n}\n\n@Composable\nfun VideoPartRow(\n    modifier: Modifier = Modifier,\n    pages: List<VideoPage>,\n    lastPlayedCid: Long = 0,\n    lastPlayedTime: Int = 0,\n    enablePartListDialog: Boolean = false,\n    nested: Boolean = false,\n    subtitle: String = \"\",\n    onClick: (cid: Long) -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n    var hasFocus by remember { mutableStateOf(false) }\n    var showPartListDialog by remember { mutableStateOf(false) }\n    val listState = rememberLazyListState()\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (hasFocus) 30f else 14f,\n        label = \"title font size\",\n        animationSpec = tween(\n            durationMillis = 120\n        )\n    )\n\n    // 滚动到有历史记录的那一集\n    LaunchedEffect(lastPlayedCid, pages) {\n        if (lastPlayedCid != 0L && pages.isNotEmpty()) {\n            val index = pages.indexOfFirst { it.cid == lastPlayedCid }\n            if (index > 0) {\n                listState.scrollToItem(index)\n            }\n        }\n    }\n\n    Column(\n        modifier = modifier\n            .ifElse(!nested, Modifier.padding(start = 26.dp))\n            .onFocusChanged { hasFocus = it.hasFocus },\n        verticalArrangement = Arrangement.SpaceBetween\n    ) {\n        Row(\n            modifier = Modifier.padding(start = 10.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Text(\n                text = stringResource(R.string.video_info_part_row_title)\n                        + (\" - $subtitle\".takeIf { subtitle.isNotBlank() } ?: \"\"),\n                fontSize = titleFontSize.sp,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis\n            )\n            if (enablePartListDialog) {\n                VideoPartRowButton(\n                    hasFocus = hasFocus,\n                    onClick = { showPartListDialog = true }\n                )\n            }\n        }\n\n        LazyRow(\n            modifier = Modifier\n                .padding(top = 4.dp)\n                .focusRestorer(focusRequester),\n            state = listState,\n            contentPadding = PaddingValues(12.dp),\n            horizontalArrangement = Arrangement.spacedBy(16.dp)\n        ) {\n            itemsIndexed(items = pages, key = { index, page -> \"$index-page-${page.cid}\" }) { index, page ->\n                VideoPartButton(\n                    modifier = Modifier\n                        .ifElse(index == 0, Modifier.focusRequester(focusRequester)),\n                    index = index + 1,\n                    title = page.title,\n                    played = if (page.cid == lastPlayedCid) lastPlayedTime else 0,\n                    isLastPlayed = page.cid == lastPlayedCid,\n                    duration = page.duration,\n                    onClick = { onClick(page.cid) }\n                )\n            }\n        }\n    }\n\n    VideoPartListDialog(\n        show = showPartListDialog,\n        onHideDialog = { showPartListDialog = false },\n        pages = pages,\n        lastPlayedCid = lastPlayedCid,\n        lastPlayedTime = lastPlayedTime,\n        title = \"分 P 列表\",\n        onClick = onClick\n    )\n}\n\n@Composable\nfun VideoUgcSeasonRow(\n    modifier: Modifier = Modifier,\n    title: String,\n    episodes: List<Episode>,\n    lastPlayedCid: Long = 0,\n    lastPlayedTime: Int = 0,\n    intentAid: Long = 0,\n    enableUgcListDialog: Boolean = false,\n    onClickEp: (avid: Long, cid: Long) -> Unit,\n    onClickEpPart: (episode: Episode, cid: Long) -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n    var hasFocus by remember { mutableStateOf(false) }\n    var showUgcListDialog by remember { mutableStateOf(false) }\n    val listState = rememberLazyListState()\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (hasFocus) 30f else 14f,\n        label = \"title font size\",\n        animationSpec = tween(\n            durationMillis = 120\n        )\n    )\n    var focusingEpisode by remember { mutableStateOf<Episode?>(null) }\n\n    // 滚动到有历史记录的那一集，如果没有历史记录则滚动到与 intentAid 相同的视频\n    LaunchedEffect(lastPlayedCid, intentAid, episodes) {\n        if (episodes.isEmpty()) return@LaunchedEffect\n\n        val index = if (lastPlayedCid != 0L) {\n            // 优先使用历史记录\n            episodes.indexOfFirst { it.cid == lastPlayedCid || it.pages.any { page -> page.cid == lastPlayedCid } }\n        } else if (intentAid != 0L) {\n            // 没有历史记录时，滚动到与 intentAid 相同的视频\n            episodes.indexOfFirst { it.aid == intentAid }\n        } else {\n            -1\n        }\n\n        if (index > 0) {\n            listState.scrollToItem(index)\n        }\n    }\n\n    Column(\n        modifier = modifier\n            .padding(start = 26.dp)\n            .onFocusChanged { hasFocus = it.hasFocus },\n        verticalArrangement = Arrangement.SpaceBetween\n    ) {\n        Row(\n            modifier = Modifier.padding(start = 10.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Text(\n                text = title,\n                fontSize = titleFontSize.sp\n            )\n            if (enableUgcListDialog) {\n                VideoPartRowButton(\n                    hasFocus = hasFocus,\n                    onClick = { showUgcListDialog = true }\n                )\n            }\n        }\n\n        LazyRow(\n            modifier = Modifier\n                .padding(top = 4.dp)\n                .focusRestorer(focusRequester),\n            state = listState,\n            contentPadding = PaddingValues(12.dp),\n            horizontalArrangement = Arrangement.spacedBy(16.dp)\n        ) {\n            itemsIndexed(\n                items = episodes,\n                key = { index, episode -> \"$index-episode-${episode.aid}-${episode.cid}\" }\n            ) { index, episode ->\n                VideoPartButton(\n                    modifier = Modifier\n                        .ifElse(index == 0, Modifier.focusRequester(focusRequester))\n                        .onFocusChanged { if (it.hasFocus) focusingEpisode = episode },\n                    index = index + 1,\n                    title = episode.title,\n                    cover = episode.cover,\n                    pubDate = episode.pubDate,\n                    played = if (episode.cid == lastPlayedCid) lastPlayedTime else 0,\n                    isLastPlayed = episode.cid == lastPlayedCid || episode.pages.any { it.cid == lastPlayedCid },\n                    isCurrentIntent = episode.aid == intentAid,\n                    duration = episode.duration,\n                    type = VideoPartType.Episode,\n                    onClick = { onClickEp(episode.aid, episode.cid) }\n                )\n            }\n        }\n\n        AnimatedVisibility((focusingEpisode?.pages?.size ?: 0) > 1) {\n            VideoPartRow(\n                modifier = Modifier.padding(top = 8.dp),\n                pages = focusingEpisode!!.pages,\n                lastPlayedCid = lastPlayedCid,\n                lastPlayedTime = lastPlayedTime,\n                enablePartListDialog = (focusingEpisode?.pages?.size ?: 0) > 5,\n                nested = true,\n                onClick = { onClickEpPart(focusingEpisode!!, it) },\n                subtitle = focusingEpisode!!.title\n            )\n        }\n    }\n\n    VideoUgcListDialog(\n        show = showUgcListDialog,\n        onHideDialog = { showUgcListDialog = false },\n        episodes = episodes,\n        lastPlayedCid = lastPlayedCid,\n        lastPlayedTime = lastPlayedTime,\n        intentAid = intentAid,\n        title = \"合集列表\",\n        onClick = onClickEp\n    )\n}\n\n@Composable\nprivate fun VideoPartListDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    title: String,\n    pages: List<VideoPage>,\n    lastPlayedCid: Long = 0,\n    lastPlayedTime: Int = 0,\n    onHideDialog: () -> Unit,\n    onClick: (cid: Long) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n\n    var selectedTabIndex by remember { mutableIntStateOf(0) }\n    val tabCount by remember { mutableIntStateOf(ceil(pages.size / 20.0).toInt()) }\n    val selectedVideoPart = remember { mutableStateListOf<VideoPage>() }\n\n    val tabFocusRequester = remember { FocusRequester() }\n    val tabRowFocusRequester = remember { FocusRequester() }\n    val videoListFocusRequester = remember { FocusRequester() }\n    val listState = rememberLazyGridState()\n\n    LaunchedEffect(selectedTabIndex) {\n        val fromIndex = selectedTabIndex * 20\n        var toIndex = (selectedTabIndex + 1) * 20\n        if (toIndex >= pages.size) {\n            toIndex = pages.size\n        }\n        selectedVideoPart.swapListWithMainContext(pages.subList(fromIndex, toIndex))\n    }\n\n    LaunchedEffect(show) {\n        if (show && tabCount > 1) tabFocusRequester.requestFocus(scope)\n        if (show && tabCount == 1) videoListFocusRequester.requestFocus(scope)\n    }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            title = { Text(text = title) },\n            onDismissRequest = { onHideDialog() },\n            confirmButton = {},\n            properties = DialogProperties(usePlatformDefaultWidth = false),\n            text = {\n                Column(\n                    modifier = Modifier.size(600.dp, 330.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    TabRow(\n                        modifier = Modifier\n                            .onFocusChanged {\n                                if (it.hasFocus) {\n                                    scope.launch(Dispatchers.Main) {\n                                        listState.scrollToItem(0)\n                                    }\n                                }\n                            }\n                            .focusRestorer()\n                            .focusRequester(tabRowFocusRequester),\n                        selectedTabIndex = selectedTabIndex,\n                        separator = { Spacer(modifier = Modifier.width(12.dp)) },\n                    ) {\n                        for (i in 0 until tabCount) {\n                            Tab(\n                                modifier = if (i == 0) Modifier.focusRequester(\n                                    tabFocusRequester\n                                ) else Modifier,\n                                selected = i == selectedTabIndex,\n                                onFocus = { selectedTabIndex = i },\n                            ) {\n                                Text(\n                                    text = \"P${i * 20 + 1}-${(i + 1) * 20}\",\n                                    fontSize = 12.sp,\n                                    color = LocalContentColor.current,\n                                    modifier = Modifier.padding(\n                                        horizontal = 16.dp,\n                                        vertical = 6.dp\n                                    )\n                                )\n                            }\n                        }\n                    }\n\n                    LazyVerticalGrid(\n                        modifier = Modifier\n                            .onBackPressed {\n                                if (tabCount > 1) tabRowFocusRequester.requestFocus() else onHideDialog()\n                            },\n                        state = listState,\n                        columns = GridCells.Fixed(2),\n                        contentPadding = PaddingValues(8.dp),\n                        verticalArrangement = Arrangement.spacedBy(8.dp),\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        itemsIndexed(\n                            items = selectedVideoPart,\n                            key = { index, video -> \"$index-video-${video.cid}\" }\n                        ) { index, page ->\n                            val buttonModifier =\n                                if (index == 0) Modifier.focusRequester(videoListFocusRequester) else Modifier\n\n                            VideoPartButton(\n                                modifier = buttonModifier,\n                                index = page.index,\n                                title = page.title,\n                                played = if (page.cid == lastPlayedCid) lastPlayedTime else 0,\n                                isLastPlayed = page.cid == lastPlayedCid,\n                                duration = page.duration,\n                                onClick = { onClick(page.cid) }\n                            )\n                        }\n                    }\n                }\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun VideoUgcListDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    title: String,\n    episodes: List<Episode>,\n    lastPlayedCid: Long = 0,\n    lastPlayedTime: Int = 0,\n    intentAid: Long = 0,\n    onHideDialog: () -> Unit,\n    onClick: (avid: Long, cid: Long) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n\n    var selectedTabIndex by remember { mutableIntStateOf(0) }\n    val tabCount by remember { mutableIntStateOf(ceil(episodes.size / 20.0).toInt()) }\n    val selectedVideoPart = remember { mutableStateListOf<Episode>() }\n\n    val tabFocusRequester = remember { FocusRequester() }\n    val tabRowFocusRequester = remember { FocusRequester() }\n    val videoListFocusRequester = remember { FocusRequester() }\n    val listState = rememberLazyGridState()\n\n    LaunchedEffect(selectedTabIndex) {\n        val fromIndex = selectedTabIndex * 20\n        var toIndex = (selectedTabIndex + 1) * 20\n        if (toIndex >= episodes.size) {\n            toIndex = episodes.size\n        }\n        selectedVideoPart.swapListWithMainContext(episodes.subList(fromIndex, toIndex))\n    }\n\n    LaunchedEffect(show) {\n        if (show && tabCount > 1) tabFocusRequester.requestFocus(scope)\n        if (show && tabCount == 1) videoListFocusRequester.requestFocus(scope)\n    }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            title = { Text(text = title) },\n            onDismissRequest = { onHideDialog() },\n            confirmButton = {},\n            properties = DialogProperties(usePlatformDefaultWidth = false),\n            text = {\n                Column(\n                    modifier = Modifier.size(640.dp, 330.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    TabRow(\n                        modifier = Modifier\n                            .onFocusChanged {\n                                if (it.hasFocus) {\n                                    scope.launch(Dispatchers.Main) {\n                                        listState.scrollToItem(0)\n                                    }\n                                }\n                            }\n                            .focusRestorer()\n                            .focusRequester(tabRowFocusRequester),\n                        selectedTabIndex = selectedTabIndex,\n                        separator = { Spacer(modifier = Modifier.width(12.dp)) },\n                    ) {\n                        for (i in 0 until tabCount) {\n                            Tab(\n                                modifier = if (i == 0) Modifier.focusRequester(\n                                    tabFocusRequester\n                                ) else Modifier,\n                                selected = i == selectedTabIndex,\n                                onFocus = { selectedTabIndex = i },\n                            ) {\n                                Text(\n                                    text = \"EP${i * 20 + 1}-${(i + 1) * 20}\",\n                                    fontSize = 12.sp,\n                                    color = LocalContentColor.current,\n                                    modifier = Modifier.padding(\n                                        horizontal = 16.dp,\n                                        vertical = 6.dp\n                                    )\n                                )\n                            }\n                        }\n                    }\n\n                    LazyVerticalGrid(\n                        modifier = Modifier\n                            .onBackPressed {\n                                if (tabCount > 1) tabRowFocusRequester.requestFocus() else onHideDialog()\n                            },\n                        state = listState,\n                        columns = GridCells.Fixed(2),\n                        contentPadding = PaddingValues(8.dp),\n                        verticalArrangement = Arrangement.spacedBy(8.dp),\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        itemsIndexed(\n                            items = selectedVideoPart,\n                            key = { index, video -> \"$index-video-${video.cid}\" }\n                        ) { index, episode ->\n                            val buttonModifier =\n                                if (index == 0) Modifier.focusRequester(videoListFocusRequester) else Modifier\n\n                            VideoPartButton(\n                                modifier = buttonModifier,\n                                index = selectedTabIndex * 20 + index + 1,\n                                type = VideoPartType.Episode,\n                                title = episode.title,\n                                cover = episode.cover,\n                                pubDate = episode.pubDate,\n                                played = if (episode.cid == lastPlayedCid) lastPlayedTime else 0,\n                                isLastPlayed = episode.cid == lastPlayedCid || episode.pages.any { it.cid == lastPlayedCid },\n                                isCurrentIntent = episode.aid == intentAid,\n                                duration = episode.duration,\n                                onClick = { onClick(episode.aid, episode.cid) }\n                            )\n                        }\n                    }\n                }\n            }\n        )\n    }\n}\n\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun VideoPartButtonShortTextPreview() {\n    BVTheme {\n        VideoPartButton(\n            index = 2,\n            title = \"这是一段短文字\",\n            duration = 100,\n            onClick = {}\n        )\n    }\n}\n\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun VideoPartButtonLongTextPreview() {\n    BVTheme {\n        VideoPartButton(\n            index = 2,\n            title = \"这可能是我这辈子距离梅西最近的一次\",\n            played = 23333,\n            duration = 100,\n            onClick = {}\n        )\n    }\n}\n\n\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun VideoPartRowPreview() {\n    val pages = remember { mutableStateListOf<VideoPage>() }\n    for (i in 0..10) {\n        pages.add(\n            VideoPage(\n                cid = 1000L + i,\n                index = i,\n                title = \"这可能是我这辈子距离梅西最近的一次\",\n                duration = 10,\n                dimension = Dimension(0, 0)\n            )\n        )\n    }\n    BVTheme {\n        VideoPartRow(pages = pages, onClick = {})\n    }\n}\n\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun UpButtonPreview() {\n    var followed by remember { mutableStateOf(false) }\n    BVTheme {\n        UpButton(\n            name = \"12435678\",\n            followed = followed,\n            onClickUp = { followed = !followed },\n            onAddFollow = {},\n            onDelFollow = {}\n        )\n    }\n}\n\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun CoverPreview() {\n    Box {\n        AsyncImage(\n            modifier = Modifier\n                .fillMaxSize(),\n            // model = if (videoDetail.ugcSeason != null) videoDetail.ugcSeason!!.cover else videoDetail.cover,\n            model = \"http://i2.hdslb.com/bfs/archive/af17fc07b8f735e822563cc45b7b5607a491dfff.jpg\",\n            contentDescription = null,\n            contentScale = ContentScale.Crop\n        )\n        Box(\n            modifier = Modifier\n                .align(Alignment.BottomCenter)\n                .fillMaxWidth()\n                .height(48.dp)\n                .clip(\n                    RoundedCornerShape(\n                        topStart = 0.dp,\n                        topEnd = 0.dp,\n                        bottomStart = 16.dp,\n                        bottomEnd = 16.dp\n                    )\n                )\n                .background(\n                    Brush.verticalGradient(\n                        colors = listOf(\n                            Color.Transparent,\n                            Color.Black.copy(alpha = 0.8f)\n                        )\n                    )\n                )\n        ) {\n            Row(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(start = 16.dp, end = 16.dp, bottom = 12.dp),\n                verticalAlignment = Alignment.Bottom,\n                horizontalArrangement = Arrangement.spacedBy(2.dp)\n            ) {\n                Icon(\n                    modifier = Modifier,\n                    painter = painterResource(id = R.drawable.ic_play_count),\n                    contentDescription = null,\n                    tint = Color.White\n                )\n                Text(\n                    text = \"3009\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = Color.White\n                )\n                Spacer(modifier = Modifier.width(8.dp))\n                Icon(\n                    modifier = Modifier,\n                    painter = painterResource(id = R.drawable.ic_danmaku_count),\n                    contentDescription = null,\n                    tint = Color.White\n                )\n                Text(\n                    text = \"1099\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = Color.White\n                )\n\n                Spacer(Modifier.weight(1f))\n                Text(\n                    text = \"12:34\",\n                    color = Color.White,\n                    style = MaterialTheme.typography.bodySmall\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/VideoPlayerV3Screen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens\n\nimport android.app.Activity\nimport android.widget.Toast\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Border\nimport androidx.tv.material3.ButtonDefaults\nimport androidx.tv.material3.MaterialTheme\nimport dev.aaa1115910.bv.tv.activities.video.SeasonInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.danmaku.DanmakuView\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerDanmakuMasksData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerHistoryData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerLoadStateData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerLogsData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerPaymentData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekThumbData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerVideoInfoData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerVideoShotData\nimport dev.aaa1115910.bv.player.entity.PortraitVideoFixMode\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoListItemData\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerDanmakuMasksData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerHistoryData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerLoadStateData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerLogsData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerPaymentData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerSeekThumbData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerVideoInfoData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerVideoShotData\nimport dev.aaa1115910.bv.player.tv.BvPlayer\nimport dev.aaa1115910.bv.player.tv.controller.LiveViewerCountTip\nimport dev.aaa1115910.bv.player.tv.controller.OnlineViewerCountTip\nimport dev.aaa1115910.bv.player.tv.controller.SkipTip\nimport dev.aaa1115910.bv.player.tv.controller.UserActionKey\nimport dev.aaa1115910.bv.tv.activities.video.TagActivity\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.component.buttons.CoinButton\nimport dev.aaa1115910.bv.tv.component.CommentPanel\nimport dev.aaa1115910.bv.tv.component.DescriptionPanel\nimport dev.aaa1115910.bv.tv.component.buttons.FavoriteButton\nimport dev.aaa1115910.bv.tv.component.buttons.LikeButton\nimport dev.aaa1115910.bv.tv.component.buttons.ToViewButton\nimport dev.aaa1115910.bv.tv.manager.FollowStateManager\nimport dev.aaa1115910.bv.tv.manager.PlayedAidsCache\nimport dev.aaa1115910.bv.tv.manager.VideoUserActionManager\nimport dev.aaa1115910.bv.tv.manager.VideoUserActionManager.getStateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport dev.aaa1115910.bv.util.swapList\nimport dev.aaa1115910.bv.viewmodel.VideoPlayerV3ViewModel\nimport dev.aaa1115910.bv.tv.component.GeetestTvVerifyDialog\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.bv.player.entity.NextVideoStrategy\nimport dev.aaa1115910.bv.tv.component.videocard.TabbedVideosPanel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun VideoPlayerV3Screen(\n    modifier: Modifier = Modifier,\n    playerViewModel: VideoPlayerV3ViewModel = koinViewModel(),\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger { }\n\n    // 外部创建 DanmakuView，与 videoPlayer 一致的模式\n    val danmakuView = remember { DanmakuView(context).also { playerViewModel.danmakuView = it } }\n\n    DisposableEffect(danmakuView) {\n        onDispose {\n            danmakuView?.release()\n        }\n    }\n\n    // subscribe shared action state by aid\n    val currentAid = playerViewModel.currentAid\n    val sharedActionFlow = remember(currentAid) { getStateFlow(currentAid, Prefs.uid) }\n    val sharedActionState by sharedActionFlow.collectAsState()\n    val followStateMap by FollowStateManager.followStateMap.collectAsState()\n\n    LaunchedEffect(followStateMap, playerViewModel.upId) {\n        val currentUpId = playerViewModel.upId\n        if (currentUpId > 0) {\n            val result = FollowStateManager.ensureFollowState(currentUpId)\n            if (result != null && playerViewModel.isFollowingUp != result) {\n                playerViewModel.isFollowingUp = result\n            }\n        }\n    }\n\n    // 倒计时相关状态\n    var autoActionCountdownJob by remember { mutableStateOf<Job?>(null) }\n    var autoActionTipVisible by remember { mutableStateOf(false) }\n    var autoActionTipText by remember { mutableStateOf(\"\") }\n    var skipNextKeyUpCancel by remember { mutableStateOf(false) }\n    var showDebugInfo by remember { mutableStateOf(Prefs.playerShowDebugInfo) }\n\n    // 在线观看人数状态\n    var onlineViewerCount by remember { mutableStateOf(\"\") }\n    var showOnlineViewerCountTip by remember { mutableStateOf(false) }\n    var canShowViewerCountTip by remember { mutableStateOf(true) }\n    var showLiveViewerCountTip by remember { mutableStateOf(false) }\n    var viewerCountText by remember { mutableStateOf(\"\") }\n\n    // 评论面板状态\n    var showCommentPanel by remember { mutableStateOf(false) }\n\n    // 简介面板状态\n    var showDescriptionPanel by remember { mutableStateOf(false) }\n\n    // 焦点管理\n    val relatedVideosFocusRequester = remember { FocusRequester() }\n\n    // 当显示相关视频时，自动将焦点转移到VideosRow的第一个卡片\n    LaunchedEffect(playerViewModel.showRelatedVideos) {\n        if (playerViewModel.showRelatedVideos) {\n            delay(300)\n            kotlin.runCatching {\n                relatedVideosFocusRequester.requestFocus()\n            }\n        }\n    }\n\n    // 获取在线观看人数\n    LaunchedEffect(playerViewModel.currentCid, playerViewModel.currentAid) {\n        if (playerViewModel.currentCid > 0 && playerViewModel.currentAid > 0 && Prefs.showOnlineViewerCount > 0) {\n            withContext(Dispatchers.IO) {\n                try {\n                    val response = BiliHttpApi.getVideoOnlineTotal(\n                        cid = playerViewModel.currentCid,\n                        aid = playerViewModel.currentAid\n                    )\n                    if (response.code == 0) {\n                        onlineViewerCount = response.data?.total ?: \"\"\n                        showOnlineViewerCountTip = true\n\n                        // 如果设置为 30 秒后隐藏，则自动隐藏\n                        if (Prefs.showOnlineViewerCount == 1) {\n                            delay(30_000)\n                            showOnlineViewerCountTip = false\n                        }\n                    }\n                } catch (e: Exception) {\n                    logger.warn(e) { \"Failed to get online viewer count\" }\n                }\n            }\n        } else {\n            onlineViewerCount = \"\"\n        }\n    }\n\n    // 在线观看人数设置为30秒后隐藏或者始终显示，每 5 分钟刷新一次数据。虽然左下角隐藏，但播放器控制条中还要显示\n    LaunchedEffect(showOnlineViewerCountTip, Prefs.showOnlineViewerCount) {\n        if (showOnlineViewerCountTip) {\n            while (true) {\n                delay(300_000)  // 5 分钟\n                if (playerViewModel.currentCid > 0 && playerViewModel.currentAid > 0) {\n                    withContext(Dispatchers.IO) {\n                        try {\n                            val response = BiliHttpApi.getVideoOnlineTotal(\n                                cid = playerViewModel.currentCid,\n                                aid = playerViewModel.currentAid\n                            )\n                            if (response.code == 0) {\n                                onlineViewerCount = response.data?.total ?: \"\"\n                            }\n                        } catch (e: Exception) {\n                            logger.warn(e) { \"Failed to refresh online viewer count\" }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    // 控制直播人气显示\n    LaunchedEffect(playerViewModel.isLive, Prefs.showLiveViewerCountTip, playerViewModel.livePopularityText) {\n        if (playerViewModel.isLive && Prefs.showLiveViewerCountTip > 0 && playerViewModel.livePopularityText.isNotEmpty()) {\n            showLiveViewerCountTip = true\n            if (Prefs.showLiveViewerCountTip == 1) {\n                delay(30_000)\n                showLiveViewerCountTip = false\n            }\n        } else {\n            showLiveViewerCountTip = false\n        }\n    }\n\n    // 更新 viewerCountText\n    LaunchedEffect(Prefs.showOnlineViewerCount, onlineViewerCount, Prefs.showOnlineViewerCount, playerViewModel.livePopularityText, playerViewModel.liveOnlineCount) {\n        if (playerViewModel.isLive && Prefs.showOnlineViewerCount > 0) {\n            if (playerViewModel.livePopularityText.isNotEmpty()) {\n                viewerCountText = playerViewModel.livePopularityText\n            }\n            if (playerViewModel.liveOnlineCount.isNotEmpty()) {\n                viewerCountText = viewerCountText + \"  ·  \" + playerViewModel.liveOnlineCount\n            }\n        } else if (Prefs.showOnlineViewerCount > 0 && onlineViewerCount.isNotEmpty()) {\n            viewerCountText = \"$onlineViewerCount 人在看\"\n        }\n    }\n\n    // 处理back键，当推荐视频有焦点时隐藏推荐视频并将焦点返回到播放器\n    BackHandler(enabled = playerViewModel.showRelatedVideos) {\n        playerViewModel.showRelatedVideos = false\n    }\n\n    CompositionLocalProvider(\n        LocalVideoPlayerSeekThumbData provides VideoPlayerSeekThumbData(\n            idleIcon = playerViewModel.playerIconIdle,\n            movingIcon = playerViewModel.playerIconMoving\n        ),\n        LocalVideoPlayerVideoInfoData provides VideoPlayerVideoInfoData(\n            width = playerViewModel.currentVideoWidth,\n            height = playerViewModel.currentVideoHeight,\n            codec = playerViewModel.currentVideoCodec.name,\n            title = playerViewModel.title,\n            partTitle = playerViewModel.partTitle,\n            play = playerViewModel.play,\n            danmaku = playerViewModel.danmaku,\n            like = playerViewModel.like,\n            coin = playerViewModel.coin,\n            favorite = playerViewModel.favorite,\n            upName = playerViewModel.upName,\n            pubTime = playerViewModel.pubTime,\n            fromSeason = playerViewModel.fromSeason,\n            isFollowingUp = playerViewModel.isFollowingUp,\n            isVerticalVideo = playerViewModel.isVerticalVideo,\n            isLive = playerViewModel.isLive\n        ),\n        LocalVideoPlayerLogsData provides VideoPlayerLogsData(\n            logs = playerViewModel.logs\n        ),\n        LocalVideoPlayerHistoryData provides VideoPlayerHistoryData(\n            lastPlayed = playerViewModel.lastPlayed,\n        ),\n        LocalVideoPlayerPaymentData provides VideoPlayerPaymentData(\n            needPay = playerViewModel.needPay,\n            epid = playerViewModel.epid,\n            showPreviewTip = playerViewModel.showPreviewTip,\n        ),\n        LocalVideoPlayerLoadStateData provides VideoPlayerLoadStateData(\n            loadState = playerViewModel.loadState,\n            errorMessage = playerViewModel.errorMessage,\n        ),\n        LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n            availableResolutions = playerViewModel.availableQuality,\n            availableVideoCodec = playerViewModel.availableVideoCodec,\n            availableAudio = playerViewModel.availableAudio,\n            availableSubtitleTracks = playerViewModel.availableSubtitle,\n            availableVideoList = playerViewModel.availableVideoList,\n            currentVideoCid = playerViewModel.currentCid,\n            currentResolution = playerViewModel.currentQuality,\n            currentVideoCodec = playerViewModel.currentVideoCodec,\n            currentVideoAspectRatio = playerViewModel.currentVideoAspectRatio,\n            currentVideoRotation = playerViewModel.currentVideoRotation,\n            currentVideoSpeed = playerViewModel.currentPlaySpeed,\n            currentAudio = playerViewModel.currentAudio,\n            currentDanmakuEnabled = playerViewModel.currentDanmakuEnabled,\n            currentDanmakuEnabledList = playerViewModel.currentDanmakuTypes,\n            currentDanmakuScale = playerViewModel.currentDanmakuScale,\n            currentDanmakuOpacity = playerViewModel.currentDanmakuOpacity,\n            currentDanmakuArea = playerViewModel.currentDanmakuArea,\n            currentDanmakuMask = playerViewModel.currentDanmakuMask,\n            currentDanmakuRollingDurationFactor = playerViewModel.currentDanmakuRollingDurationFactor,\n            currentDanmakuFilterLevel = playerViewModel.currentDanmakuFilterLevel,\n            currentLiveDanmakuFilterLevel = playerViewModel.currentLiveDanmakuFilterLevel,\n            currentSubtitleId = playerViewModel.currentSubtitleId,\n            currentSubtitleData = playerViewModel.currentSubtitleData,\n            currentSubtitleFontSize = playerViewModel.currentSubtitleFontSize,\n            currentSubtitleBackgroundOpacity = playerViewModel.currentSubtitleBackgroundOpacity,\n            currentSubtitleBottomPadding = playerViewModel.currentSubtitleBottomPadding,\n            currentPlayMode = playerViewModel.currentPlayMode,\n            incognitoMode = Prefs.incognitoMode,\n            hasPreloadedVideoList = playerViewModel.preloadedVideoList.isNotEmpty(),\n            hasRelatedVideos = playerViewModel.relatedVideos.isNotEmpty(),\n            fromSeason = playerViewModel.fromSeason,\n            showDanmaku = playerViewModel.showDanmaku,\n            showRelatedVideos = playerViewModel.showRelatedVideos,\n            showNextVideoBtn = !(playerViewModel.currentPlayMode == PlayMode.SingleVideo || playerViewModel.currentPlayMode == PlayMode.SingleLoop || (playerViewModel.currentPlayMode == PlayMode.Custom && Prefs.playerNextVideoStrategyOrder.split(\",\").none { !it.startsWith(\"-\") })),\n            defaultStartPosition = Prefs.playerDefaultStartPosition.toPlayerType(),\n            clipInfoList = playerViewModel.clipInfoList,\n            skipPgcIntroOutro = Prefs.skipPgcIntroOutro,\n            isLive = playerViewModel.isLive,\n            availableLiveQualities = playerViewModel.availableLiveQualities.toList(),\n            currentLiveQn = playerViewModel.currentLiveQn,\n            currentLiveQualityDescription = playerViewModel.currentLiveQualityDescription,\n            currentLiveCodec = playerViewModel.currentLiveCodec,\n            controllerButtonsOrder = Prefs.playerControllerButtonsOrder,\n            showDebugInfo = showDebugInfo,\n            longPressAction = Prefs.playerLongPressAction,\n            longPressSpeed = Prefs.playerLongPressSpeed\n        ),\n        LocalVideoPlayerDanmakuMasksData provides VideoPlayerDanmakuMasksData(\n            danmakuMasks = playerViewModel.danmakuMasks,\n        ),\n        LocalVideoPlayerVideoShotData provides VideoPlayerVideoShotData(\n            videoShot = playerViewModel.videoShot,\n        ),\n    ) {\n        Box(\n            modifier = Modifier\n                .onPreviewKeyEvent { keyEvent ->\n                    // 检测长按下键，标记跳过对应的 KeyUp 取消\n                    if (keyEvent.type == KeyEventType.KeyDown && keyEvent.key == Key.DirectionDown\n                        && keyEvent.nativeKeyEvent.isLongPress) {\n                        skipNextKeyUpCancel = true\n                    }\n                    if (keyEvent.type == KeyEventType.KeyUp && autoActionCountdownJob != null) {\n                        // 跳过长按下键触发的那次 KeyUp（长按下键释放）\n                        if (skipNextKeyUpCancel) {\n                            skipNextKeyUpCancel = false\n                            return@onPreviewKeyEvent false\n                        }\n                        // 任何按键都可以取消倒计时\n                        logger.debug { \"按下按键: ${keyEvent.key}, 取消播放下一个（或自动退出）\" }\n                        autoActionCountdownJob?.cancel()\n                        autoActionCountdownJob = null\n                        autoActionTipVisible = false\n                        return@onPreviewKeyEvent keyEvent.key == Key.Back\n                    }\n                    false\n                }\n        ) {\n            BvPlayer(\n                modifier = modifier\n                    .fillMaxSize(),\n                videoPlayer = playerViewModel.videoPlayer!!,\n                playerSeekForwardStep = Prefs.playerSeekForwardStep,\n                playerSeekBackwardStep = Prefs.playerSeekBackwardStep,\n                showBottomProgressBar = Prefs.playerShowBottomProgressBar,\n                useTextureViewFixPortraitVideo = Prefs.portraitVideoFixMode == PortraitVideoFixMode.UseTextureView && playerViewModel.isVerticalVideo && playerViewModel.currentQuality >= Resolution.R4K,\n                onViewerCountTipCanShowChanged = { canShow ->\n                    if (canShowViewerCountTip != canShow) {\n                        canShowViewerCountTip = canShow\n                    }\n                },\n                viewerCountText = viewerCountText,\n                danmakuView = danmakuView,\n                onToggleRelatedVideos = { state ->\n                    playerViewModel.showRelatedVideos = if (playerViewModel.relatedVideos.isNotEmpty() || playerViewModel.preloadedVideoList.isNotEmpty()) state else false\n                },\n                onSendHeartbeat = playerViewModel::uploadHistory,\n                onClearBackToHistoryData = { playerViewModel.lastPlayed = 0 },\n                onLoadNextVideo = { immediate ->\n                    if (playerViewModel.showRelatedVideos) {\n                        logger.info { \"Related videos is shown, skip auto action\" }\n                        return@BvPlayer\n                    }\n\n                    if (showCommentPanel) {\n                        logger.info { \"Comment panel is shown, skip auto action\" }\n                        return@BvPlayer\n                    }\n\n                    // 找出下一个剧集/分P\n                    val currentIndex = playerViewModel.availableVideoList.indexOfFirst {\n                        when (it) {\n                            is VideoListItemData -> it.cid == playerViewModel.currentCid\n                            else -> false\n                        }\n                    }\n                    val nextEp =\n                        if (currentIndex >= 0 && currentIndex + 1 < playerViewModel.availableVideoList.size) {\n                            playerViewModel.availableVideoList\n                                .drop(currentIndex + 1)\n                                .firstOrNull { it is VideoListItemData } as? VideoListItemData\n                        } else null\n\n                    // 找出上一个剧集/分P（逆序模式用）\n                    val prevEp =\n                        if (currentIndex > 0) {\n                            playerViewModel.availableVideoList\n                                .take(currentIndex)\n                                .lastOrNull { it is VideoListItemData } as? VideoListItemData\n                        } else null\n\n                    // 标记当前稿件已播放\n                    PlayedAidsCache.markPlayed(playerViewModel.currentAid)\n\n                    // 找出下一个推荐视频（非充电、非播放过的aid）\n                    val candidates = playerViewModel.relatedVideos\n                        .filter { related -> !related.isChargingArc && !PlayedAidsCache.hasPlayed(related.avid) }\n                        .take(10)\n                    val nextRelatedVideo = if (candidates.isNotEmpty()) candidates.random() else null\n\n                    // 找出预加载列表的下一个\n                    val preloaded = playerViewModel.preloadedVideoList\n                    val preloadIndex = playerViewModel.resolveLastPreloadedVideoIndex()\n                    val nextPreloaded = if (preloadIndex >= 0 && preloadIndex + 1 < preloaded.size) {\n                        preloaded[preloadIndex + 1]\n                    } else null\n\n                    // 找出预加载列表的上一个（逆序模式用）\n                    val prevPreloaded = if (preloadIndex > 0) {\n                        preloaded[preloadIndex - 1]\n                    } else null\n\n                    // nextVideo 可以是分P/剧集(VideoListItemData) 或推荐卡片(VideoCardData)\n                    var nextVideo: Any? = null\n\n                    when (playerViewModel.currentPlayMode) {\n                        PlayMode.Custom -> {\n                            // 使用设置中的策略顺序\n                            val validOrdinals = NextVideoStrategy.entries.map { it.ordinalValue }.toSet()\n                            val strategies = Prefs.playerNextVideoStrategyOrder.split(\",\").filter { !it.startsWith(\"-\") }.mapNotNull { val id = it.toIntOrNull() ?: return@mapNotNull null; if (id !in validOrdinals) return@mapNotNull null; NextVideoStrategy.fromOrdinal(id) }\n                            for (strategy in strategies) {\n                                if (strategy == NextVideoStrategy.SingleVideo) {\n                                    // 单视频模式：不自动播放下一个\n                                    break\n                                } else if (strategy == NextVideoStrategy.PartAndEpisode) {\n                                    if (nextEp != null) { nextVideo = nextEp; break }\n                                } else if (strategy == NextVideoStrategy.PreloadedVideoList) {\n                                    if (nextPreloaded != null) { nextVideo = nextPreloaded; break }\n                                } else if (strategy == NextVideoStrategy.RelatedVideo) {\n                                    if (nextRelatedVideo != null) { nextVideo = nextRelatedVideo; break }\n                                } else if (strategy == NextVideoStrategy.PartAndEpisodeReverse) {\n                                    if (prevEp != null) { nextVideo = prevEp; break }\n                                } else if (strategy == NextVideoStrategy.PreloadedVideoListReverse) {\n                                    if (prevPreloaded != null) { nextVideo = prevPreloaded; break }\n                                }\n                            }\n                        }\n                        PlayMode.SingleVideo -> {\n                            // 单视频模式：不自动播放下一个\n                            logger.info { \"PlayMode.SingleVideo: no auto next\" }\n                        }\n                        PlayMode.SingleLoop -> {\n                            // BvPlayer.onEnd 已处理循环，这里不应到达\n                            logger.info { \"PlayMode.SingleLoop: should not reach onLoadNextVideo\" }\n                        }\n                        PlayMode.ListOrder -> {\n                            if (nextPreloaded != null) {\n                                playerViewModel.resolveLastPreloadedVideoIndex(nextPreloaded.avid)\n                            }\n                            nextVideo = nextPreloaded\n                        }\n                        PlayMode.ListOrderReverse -> {\n                            if (prevPreloaded != null) {\n                                playerViewModel.resolveLastPreloadedVideoIndex(prevPreloaded.avid)\n                            }\n                            nextVideo = prevPreloaded\n                        }\n                        PlayMode.PartAndEpisode -> {\n                            nextVideo = nextEp\n                        }\n                        PlayMode.PartAndEpisodeReverse -> {\n                            nextVideo = prevEp\n                        }\n                        PlayMode.RelatedVideo -> {\n                            nextVideo = nextRelatedVideo\n                        }\n                    }\n\n                    if (nextVideo != null) {\n                        autoActionCountdownJob = scope.launch {\n                            try {\n                                if (!immediate) {\n                                    autoActionTipText = \"即将播放下一个\"\n                                    autoActionTipVisible = true\n                                    delay(1380)\n                                }\n                                autoActionTipVisible = false\n                                if (autoActionCountdownJob != null) {\n                                    autoActionCountdownJob = null\n                                    when (nextVideo) {\n                                        is VideoListItemData -> {\n                                            PlayedAidsCache.markPlayed(nextVideo.aid)\n                                            playerViewModel.title = nextVideo.title\n                                            playerViewModel.partTitle = nextVideo.partTitle\n                                            if (nextVideo.seasonId == null && playerViewModel.currentAid != nextVideo.aid) {\n                                                VideoInfoActivity.actionStart(\n                                                    context = context,\n                                                    aid = nextVideo.aid,\n                                                    cid = nextVideo.cid,\n                                                    fromPlayer = true\n                                                )\n                                            } else {\n                                                playerViewModel.loadPlayUrl(\n                                                    avid = nextVideo.aid,\n                                                    cid = nextVideo.cid!!,\n                                                    epid = nextVideo.epid,\n                                                    seasonId = nextVideo.seasonId,\n                                                    continuePlayNext = true\n                                                )\n                                            }\n                                        }\n\n                                        is VideoCardData -> {\n                                            // 推荐视频卡片：跳转到视频详情（再进入播放器）\n                                            PlayedAidsCache.markPlayed(nextVideo.avid)\n                                            if (nextVideo.jumpToSeason) {\n                                                SeasonInfoActivity.actionStart(\n                                                    context = context,\n                                                    epId = nextVideo.epId!!,\n                                                    proxyArea = ProxyArea.checkProxyArea(nextVideo.title)\n                                                )\n                                            } else {\n                                                VideoInfoActivity.actionStart(\n                                                    context = context,\n                                                    aid = nextVideo.avid,\n                                                    fromPlayer = true\n                                                )\n                                            }\n                                        }\n                                    }\n                                }\n                            } catch (_: Exception) {\n                                autoActionTipVisible = false\n                                autoActionCountdownJob = null\n                            }\n                        }\n                    } else if (Prefs.playerExitWhenAllIsPlayed) {\n                        // 没有下一个：退出\n                        autoActionCountdownJob = scope.launch {\n                            try {\n                                autoActionTipText = \"播放结束，即将退出\"\n                                autoActionTipVisible = true\n                                delay(1380)\n                                autoActionTipVisible = false\n                                if (autoActionCountdownJob != null) {\n                                    autoActionCountdownJob = null\n                                    Prefs.currentPlaySpeed = Prefs.defaultPlaySpeed\n                                    // 自动退出时也清空缓存\n                                    PlayedAidsCache.clear()\n                                    (context as Activity).finish()\n                                }\n                            } catch (_: Exception) {\n                                autoActionTipVisible = false\n                                autoActionCountdownJob = null\n                            }\n                        }\n                    }\n                    // 什么都不做\n                },\n                onExit = {\n                    Prefs.currentPlaySpeed = Prefs.defaultPlaySpeed\n                    // 退出时清空播放缓存\n                    PlayedAidsCache.clear()\n                    (context as Activity).finish()\n                },\n                onLoadNewVideo = { videoListItem ->\n                    when (videoListItem) {\n                        is VideoListItemData -> {\n                            // 手动选择新视频时也标记播放\n                            PlayedAidsCache.markPlayed(videoListItem.aid)\n                            playerViewModel.title = videoListItem.title\n                            playerViewModel.partTitle = videoListItem.partTitle\n                            if (videoListItem.seasonId == null && playerViewModel.currentAid != videoListItem.aid) {\n                                VideoInfoActivity.actionStart(\n                                    context = context,\n                                    aid = videoListItem.aid,\n                                    cid = videoListItem.cid,\n                                    fromPlayer = true\n                                )\n                            } else {\n                                playerViewModel.loadPlayUrl(\n                                    avid = videoListItem.aid,\n                                    cid = videoListItem.cid!!,\n                                    epid = videoListItem.epid,\n                                    seasonId = videoListItem.seasonId,\n                                    continuePlayNext = true\n                                )\n                            }\n                        }\n                    }\n                },\n                onRefreshVideo = {\n                    if (playerViewModel.isLive) {\n                        // 直播模式：重新获取直播流 URL\n                        logger.info { \"Reload live stream for room ${playerViewModel.liveRoomId}\" }\n                        playerViewModel.loadLiveStreamWithQuality(\n                            playerViewModel.liveRoomId,\n                            playerViewModel.currentLiveQn\n                        )\n                    } else {\n                        val time = playerViewModel.videoPlayer?.currentPosition ?: 0\n                        logger.info { \"Reload video and back to time: ${time.formatHourMinSec()}\" }\n                        scope.launch {\n                            playerViewModel.playQuality()\n                            playerViewModel.videoPlayer?.seekTo(time)\n                            playerViewModel.danmakuView?.notifySeek(time)\n                            playerViewModel.videoPlayer?.start()\n                            Toast.makeText(\n                                context,\n                                \"已刷新\\nVideo Host: ${playerViewModel.lastVideoHost}\\nAudio Host: ${playerViewModel.lastAudioHost}\",\n                                Toast.LENGTH_SHORT\n                            ).show()\n                        }\n                    }\n                },\n                onLiveRetry = {\n                    playerViewModel.retryLiveStream()\n                },\n                onShowComment = { showCommentPanel = true },\n                onShowDescription = { showDescriptionPanel = true },\n                onResolutionChange = { resolutionCode, afterChange ->\n                    scope.launch(Dispatchers.Default) {\n                        playerViewModel.playQuality(resolutionCode)\n                        afterChange()\n                        playerViewModel.currentQuality = resolutionCode\n                    }\n                },\n                onCodecChange = { videoCodec, afterChange ->\n                    playerViewModel.currentVideoCodec = videoCodec\n                    scope.launch(Dispatchers.Default) {\n                        playerViewModel.playQuality(\n                            playerViewModel.currentQuality,\n                            playerViewModel.currentVideoCodec\n                        )\n                        afterChange()\n                    }\n                },\n                onAspectRatioChange = { aspectRatio ->\n                    playerViewModel.currentVideoAspectRatio = aspectRatio\n                },\n                onRotationChange = { rotation ->\n                    playerViewModel.currentVideoRotation = rotation\n                },\n                onPlaySpeedChange = { speed ->\n                    Prefs.currentPlaySpeed = speed\n                    playerViewModel.currentPlaySpeed = speed\n                },\n                onAudioChange = { audio, afterChange ->\n                    playerViewModel.currentAudio = audio\n                    scope.launch(Dispatchers.Default) {\n                        playerViewModel.playQuality(audio = audio)\n                        afterChange()\n                    }\n                },\n                onLiveQualityChange = { qn ->\n                    playerViewModel.changeLiveQuality(qn)\n                },\n                onLiveCodecChange = { codec ->\n                    println(\"VideoPlayerV3Screen: onLiveCodecChange called with codec=$codec\")\n                    playerViewModel.changeLiveCodec(codec)\n                },\n                onDanmakuSwitchChange = { enabledDanmakuTypes ->\n                    Prefs.defaultDanmakuTypes = enabledDanmakuTypes\n                    playerViewModel.currentDanmakuTypes.swapList(enabledDanmakuTypes)\n                },\n                onDanmakuSizeChange = { scale ->\n                    Prefs.defaultDanmakuScale = scale\n                    playerViewModel.currentDanmakuScale = scale\n                },\n                onDanmakuOpacityChange = { opacity ->\n                    Prefs.defaultDanmakuOpacity = opacity\n                    playerViewModel.currentDanmakuOpacity = opacity\n                },\n                onDanmakuAreaChange = { area ->\n                    Prefs.defaultDanmakuArea = area\n                    playerViewModel.currentDanmakuArea = area\n                },\n                onDanmakuMaskChange = { mask ->\n                    Prefs.defaultDanmakuMask = mask\n                    playerViewModel.currentDanmakuMask = mask\n                },\n                onDanmakuRollingDurationFactorChange = { factor ->\n                    Prefs.defaultDanmakuRollingDurationFactor = factor\n                    playerViewModel.currentDanmakuRollingDurationFactor = factor\n                },\n                onDanmakuFilterLevelChange = { filterLevel ->\n                    if (playerViewModel.isLive) {\n                        Prefs.defaultLiveDanmakuFilterLevel = filterLevel\n                        playerViewModel.currentLiveDanmakuFilterLevel = filterLevel\n                    } else {\n                        Prefs.defaultDanmakuFilterLevel = filterLevel\n                        playerViewModel.currentDanmakuFilterLevel = filterLevel\n                    }\n                },\n                onSubtitleChange = { subtitle ->\n                    playerViewModel.loadSubtitle(subtitle.id)\n                },\n                onSubtitleSizeChange = { size ->\n                    Prefs.defaultSubtitleFontSize = size\n                    playerViewModel.currentSubtitleFontSize = size\n                },\n                onSubtitleBackgroundOpacityChange = { opacity ->\n                    Prefs.defaultSubtitleBackgroundOpacity = opacity\n                    playerViewModel.currentSubtitleBackgroundOpacity = opacity\n                },\n                onSubtitleBottomPadding = { padding ->\n                    Prefs.defaultSubtitleBottomPadding = padding\n                    playerViewModel.currentSubtitleBottomPadding = padding\n                },\n                onPlayModeChange = { playMode ->\n                    Prefs.defaultPlayMode = playMode\n                    playerViewModel.currentPlayMode = playMode\n                },\n                onDebugInfoChange = { enabled ->\n                    Prefs.playerShowDebugInfo = enabled\n                    showDebugInfo = enabled\n                },\n                onOpenUpSpace = {\n                    UpInfoActivity.actionStart(\n                        context,\n                        mid = playerViewModel.upId,\n                        name = playerViewModel.upName,\n                        face = playerViewModel.upFace\n                    )\n                },\n                onShowDanmakuChange = {\n                    Prefs.showDanmaku = it\n                    playerViewModel.showDanmaku = it\n                },\n                userActionContent = { \n                    modifier,\n                    focusMap, \n                    onFocus, \n                    onPauseAutoHide ->\n                    if (Prefs.isLogin && !playerViewModel.fromSeason) {\n                        // 增加操作：点赞、收藏、投币。通过 focusMap 获取 focusRequester 并在 onFocusChanged 回调时通知 controller\n                        val likeFocus = focusMap[UserActionKey.Like]\n                        val favFocus = focusMap[UserActionKey.Favorite]\n                        val coinFocus = focusMap[UserActionKey.Coin]\n                        val toViewFocus = focusMap[UserActionKey.ToView]\n\n                        Row(\n                            modifier = modifier\n                                .fillMaxWidth()\n                                .padding(start = 32.dp, bottom = 4.dp)\n                                .offset(y = 8.dp),\n                            horizontalArrangement = Arrangement.spacedBy(8.dp)\n                        ) {\n                            LikeButton(\n                                modifier = Modifier\n                                    .height(26.dp)\n                                    .onFocusChanged { if (it.isFocused) onFocus(UserActionKey.Like) }\n                                    .then(likeFocus?.let { Modifier.focusRequester(it) } ?: Modifier),\n                                contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp),\n                                colors = ButtonDefaults.colors(\n                                    containerColor = Color.Transparent,\n                                    focusedContainerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),\n                                    focusedContentColor = MaterialTheme.colorScheme.onSurface\n                                ),\n                                border = ButtonDefaults.border(\n                                    border = Border(\n                                        border = BorderStroke(\n                                            width = 1.dp,\n                                            color = Color.Transparent\n                                        )\n                                    ),\n                                    focusedBorder = Border(\n                                        border = BorderStroke(\n                                            width = 1.dp,\n                                            color = Color.White.copy(alpha = 0.45f)\n                                        )\n                                    )\n                                ),\n                                // use shared state\n                                isLike = sharedActionState.liked,\n                                onToggleLike = {\n                                    val aid = playerViewModel.currentAid\n                                    scope.launch {\n                                        val flow = getStateFlow(aid, Prefs.uid)\n                                        val current = flow.value\n                                        if (current.liked) {\n                                            val success = VideoUserActionManager.delLike(aid, Prefs.uid)\n                                            if (!success) {\n                                                \"点赞失败\".toast(context)\n                                            }\n                                        } else {\n                                            val success = VideoUserActionManager.addLike(aid, Prefs.uid)\n                                            if (!success) {\n                                                \"取消点赞失败\".toast(context)\n                                            }\n                                        }\n                                    }\n                                }\n                            )\n                            FavoriteButton(\n                                modifier = Modifier\n                                    .height(24.dp)\n                                    .onFocusChanged { if (it.isFocused) onFocus(UserActionKey.Favorite) }\n                                    .then(favFocus?.let { Modifier.focusRequester(it) } ?: Modifier),\n                                contentPadding = PaddingValues(horizontal = 6.dp, vertical = 0.dp),\n                                colors = ButtonDefaults.colors(\n                                    containerColor = Color.Transparent,\n                                    focusedContainerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),\n                                    focusedContentColor = MaterialTheme.colorScheme.onSurface\n                                ),\n                                border = ButtonDefaults.border(\n                                    border = Border(\n                                        border = BorderStroke(\n                                            width = 1.dp,\n                                            color = Color.Transparent\n                                        )\n                                    ),\n                                    focusedBorder = Border(\n                                        border = BorderStroke(\n                                            width = 1.dp,\n                                            color = Color.White.copy(alpha = 0.45f)\n                                        )\n                                    )\n                                ),\n                                dialogContainerColor = Color.Black.copy(alpha = 0.5f),\n                                isFavorite = sharedActionState.favorited,\n                                favoriteFolderIds = sharedActionState.favoriteFolderIds,\n                                onAddToDefaultFavoriteFolder = {\n                                    scope.launch {\n                                        val success = VideoUserActionManager.addToDefaultFavoriteFolder(playerViewModel.currentAid, Prefs.uid)\n                                        if (!success) {\n                                            \"收藏失败！默认收藏夹不存在？\".toast(context)\n                                        }\n                                    }\n                                },\n                                onUpdateFavoriteFolders = {\n                                    scope.launch {\n                                        val success = VideoUserActionManager.updateVideoFavoriteFolders(playerViewModel.currentAid, it, Prefs.uid)\n                                        if (!success) {\n                                            \"收藏失败！此收藏夹收藏数量已达上限（1000）\".toast(context)\n                                        }\n                                    }\n                                },\n                                onDialogVisibilityChanged = onPauseAutoHide\n                            )\n                            CoinButton(\n                                modifier = Modifier\n                                    .height(26.dp)\n                                    .onFocusChanged { if (it.isFocused) onFocus(UserActionKey.Coin) }\n                                    .then(coinFocus?.let { Modifier.focusRequester(it) } ?: Modifier),\n                                contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp),\n                                colors = ButtonDefaults.colors(\n                                    containerColor = Color.Transparent,\n                                    focusedContainerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),\n                                    focusedContentColor = MaterialTheme.colorScheme.onSurface\n                                ),\n                                border = ButtonDefaults.border(\n                                    border = Border(\n                                        border = BorderStroke(\n                                            width = 1.dp,\n                                            color = Color.Transparent\n                                        )\n                                    ),\n                                    focusedBorder = Border(\n                                        border = BorderStroke(\n                                            width = 1.dp,\n                                            color = Color.White.copy(alpha = 0.45f)\n                                        )\n                                    )\n                                ),\n                                isCoin = sharedActionState.coin,\n                                onAddCoin = {\n                                    scope.launch {\n                                        val success = VideoUserActionManager.addCoin(playerViewModel.currentAid, Prefs.uid)\n                                        withContext(Dispatchers.Main) {\n                                            if (!success) {\n                                                \"投币失败\".toast(context)\n                                            }\n                                        }\n                                    }\n                                }\n                            )\n                            ToViewButton(\n                                modifier = Modifier\n                                    .height(26.dp)\n                                    .onFocusChanged { if (it.isFocused) onFocus(UserActionKey.ToView) }\n                                    .then(toViewFocus?.let { Modifier.focusRequester(it) } ?: Modifier),\n                                contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp),\n                                colors = ButtonDefaults.colors(\n                                    containerColor = Color.Transparent,\n                                    focusedContainerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),\n                                    focusedContentColor = MaterialTheme.colorScheme.onSurface\n                                ),\n                                border = ButtonDefaults.border(\n                                    border = Border(\n                                        border = BorderStroke(\n                                            width = 1.dp,\n                                            color = Color.Transparent\n                                        )\n                                    ),\n                                    focusedBorder = Border(\n                                        border = BorderStroke(\n                                            width = 1.dp,\n                                            color = Color.White.copy(alpha = 0.45f)\n                                        )\n                                    )\n                                ),\n                                onAddToView = {\n                                    scope.launch {\n                                        val success = VideoUserActionManager.addToView(playerViewModel.currentAid, Prefs.uid)\n                                        if (success) {\n                                            \"已添加到稍后再看\".toast(context)\n                                        } else {\n                                            \"添加到稍后再看失败\".toast(context)\n                                        }\n                                    }\n                                }\n                            )\n                        }\n                    }\n                }\n            )\n\n            // 显示跳过提示\n            if (autoActionTipVisible) {\n                SkipTip(\n                    modifier = Modifier.padding(bottom = 22.dp),\n                    show = true,\n                    text = autoActionTipText,\n                    align = Alignment.BottomEnd\n                )\n            }\n            // 推荐视频 / 视频列表\n            AnimatedVisibility(\n                modifier = Modifier\n                    .align(Alignment.BottomStart)\n                    .fillMaxWidth(),\n                visible = playerViewModel.showRelatedVideos && !playerViewModel.isLive && !playerViewModel.fromSeason,\n                enter = expandVertically(),\n                exit = shrinkVertically(),\n                label = \"RelatedVideosForPlayer\"\n            ) {\n                TabbedVideosPanel(\n                    relatedVideos = playerViewModel.relatedVideos,\n                    preloadedVideos = playerViewModel.preloadedVideoList,\n                    currentAid = playerViewModel.currentAid,\n                    focusRequester = relatedVideosFocusRequester,\n                    onOpenSeasonInfo = { videoData, fromUGCList ->\n                        if (fromUGCList) {\n                            playerViewModel.resolveLastPreloadedVideoIndex(videoData.avid)\n                        }\n                        SeasonInfoActivity.actionStart(\n                            context = context,\n                            epId = videoData.epId!!,\n                            proxyArea = ProxyArea.checkProxyArea(videoData.title)\n                        )\n                    },\n                    onOpenVideoInfo = { videoData, fromUGCList ->\n                        if (fromUGCList) {\n                            playerViewModel.resolveLastPreloadedVideoIndex(videoData.avid)\n                        }\n                        VideoInfoActivity.actionStart(\n                            context = context,\n                            aid = videoData.avid,\n                            fromPlayer = true\n                        )\n                    }\n                )\n            }\n\n            // 在线观看人数 Tip\n            OnlineViewerCountTip(\n                show = showOnlineViewerCountTip && canShowViewerCountTip && !playerViewModel.showRelatedVideos,\n                count = onlineViewerCount\n            )\n\n            // 评论面板\n            if (playerViewModel.currentAid > 0) {\n                CommentPanel(\n                    show = showCommentPanel,\n                    oid = playerViewModel.currentAid,\n                    onHide = { showCommentPanel = false }\n                )\n            }\n\n            // 简介面板\n            DescriptionPanel(\n                show = showDescriptionPanel,\n                description = playerViewModel.videoDescription,\n                tags = playerViewModel.videoTags,\n                onHide = { showDescriptionPanel = false },\n                onClickTag = { tag ->\n                    TagActivity.actionStart(\n                        context = context,\n                        tagId = tag.id,\n                        tagName = tag.name\n                    )\n                }\n            )\n\n            // 直播人气 Tip（左下角常驻）\n            LiveViewerCountTip(\n                show = showLiveViewerCountTip && canShowViewerCountTip && playerViewModel.livePopularityText.isNotEmpty(),\n                popularityText = playerViewModel.livePopularityText,\n                onlineCount = playerViewModel.liveOnlineCount\n            )\n\n            // 风控 Geetest 验证弹窗（TV 遥控器十字光标 + WebView）\n            if (playerViewModel.showGeetestDialog) {\n                GeetestTvVerifyDialog(\n                    gt = playerViewModel.geetestGt,\n                    challenge = playerViewModel.geetestChallenge,\n                    onResult = { result ->\n                        playerViewModel.onGeetestResult(\n                            challenge = result.challenge,\n                            validate = result.validate,\n                            seccode = result.seccode,\n                        )\n                    },\n                    onDismiss = {\n                        playerViewModel.onGeetestCancelled()\n                    },\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/login/AppQRLoginContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.login\n\nimport android.app.Activity\nimport android.view.KeyEvent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.nativeKeyCode\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.login.QrLoginState\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.login.AppQrLoginViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun AppQRLoginContent(\n    modifier: Modifier = Modifier,\n    appQrLoginViewModel: AppQrLoginViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    LaunchedEffect(Unit) {\n        appQrLoginViewModel.requestQRCode()\n    }\n\n    LaunchedEffect(appQrLoginViewModel.state) {\n        if (appQrLoginViewModel.state == QrLoginState.Success) {\n            R.string.login_success.toast(context)\n            (context as Activity).finish()\n        }\n    }\n\n    DisposableEffect(Unit) {\n        onDispose {\n            appQrLoginViewModel.cancelCheckLoginResultTimer()\n        }\n    }\n\n    Surface(\n        modifier = Modifier.fillMaxSize()\n    ) {\n        Box(\n            modifier = modifier\n                .focusable()\n                .fillMaxSize()\n                .onKeyEvent {\n                    if (it.key.nativeKeyCode == KeyEvent.KEYCODE_DPAD_CENTER) {\n                        if (listOf(QrLoginState.Expired, QrLoginState.Error)\n                                .contains(appQrLoginViewModel.state)\n                        ) {\n                            appQrLoginViewModel.requestQRCode()\n                        }\n                        return@onKeyEvent true\n                    }\n                    false\n                },\n            contentAlignment = Alignment.Center\n        ) {\n            Column(\n                horizontalAlignment = Alignment.CenterHorizontally,\n                verticalArrangement = Arrangement.spacedBy(36.dp)\n            ) {\n                AnimatedVisibility(\n                    visible = listOf(QrLoginState.WaitingForScan, QrLoginState.WaitingForConfirm)\n                        .contains(appQrLoginViewModel.state)\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .size(240.dp)\n                            .clip(MaterialTheme.shapes.large)\n                            .background(Color.White),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        Image(\n                            modifier = Modifier.size(200.dp),\n                            bitmap = appQrLoginViewModel.qrImage,\n                            contentDescription = null\n                        )\n                    }\n                }\n\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    verticalArrangement = Arrangement.spacedBy(16.dp)\n                ) {\n                    Text(\n                        text = when (appQrLoginViewModel.state) {\n                            QrLoginState.Ready, QrLoginState.RequestingQRCode -> stringResource(R.string.login_requesting)\n                            QrLoginState.WaitingForScan -> stringResource(R.string.login_wait_for_scan)\n                            QrLoginState.WaitingForConfirm -> stringResource(R.string.login_wait_for_confirm)\n                            QrLoginState.Expired -> stringResource(R.string.login_expired)\n                            QrLoginState.Success -> stringResource(R.string.login_success)\n                            QrLoginState.Error, QrLoginState.Unknown -> stringResource(R.string.login_error)\n                        },\n                        style = MaterialTheme.typography.displaySmall,\n                    )\n                    AnimatedVisibility(\n                        visible = listOf(QrLoginState.Expired, QrLoginState.Error)\n                            .contains(appQrLoginViewModel.state)\n                    ) {\n                        Text(\n                            text = stringResource(R.string.login_retry),\n                            style = MaterialTheme.typography.displaySmall,\n                            fontSize = 26.sp\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/login/LoginScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.login\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\nfun LoginScreen(\n    modifier: Modifier = Modifier\n) {\n    /*when (Prefs.apiType) {\n        ApiType.Http -> {\n            WebQRLoginContent(modifier)\n        }\n\n        ApiType.GRPC -> {\n            SmsLoginContent(modifier)\n        }\n    }*/\n    AppQRLoginContent(modifier)\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/login/SmsLoginContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.login\n\nimport android.app.Activity\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport com.geetest.sdk.GT3ConfigBean\nimport com.geetest.sdk.GT3ErrorBean\nimport com.geetest.sdk.GT3GeetestUtils\nimport com.geetest.sdk.GT3Listener\nimport dev.aaa1115910.biliapi.repositories.SendSmsState\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.login.GeetestResult\nimport dev.aaa1115910.bv.viewmodel.login.SmsLoginViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.json.Json\nimport org.json.JSONObject\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun SmsLoginContent(\n    modifier: Modifier = Modifier,\n    smsLoginViewModel: SmsLoginViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val logger = KotlinLogging.logger { }\n    val scope = rememberCoroutineScope()\n    val keyboardController = LocalSoftwareKeyboardController.current\n\n    var gt3GeetestUtils: GT3GeetestUtils? by remember { mutableStateOf(null) }\n    val gt3ConfigBean by remember { mutableStateOf(GT3ConfigBean()) }\n    var phoneNumberText by remember { mutableStateOf(\"\") }\n    var codeText by remember { mutableStateOf(\"\") }\n\n    val setConfig: (challenge: String, gt: String) -> Unit = { challenge, gt ->\n        gt3GeetestUtils!!.startCustomFlow()\n        gt3ConfigBean.api1Json = JSONObject().apply {\n            put(\"success\", 1)\n            put(\"gt\", gt)\n            put(\"challenge\", challenge)\n        }\n        gt3GeetestUtils!!.getGeetest()\n    }\n\n    val sendSms = {\n        keyboardController?.hide()\n        scope.launch(Dispatchers.IO) {\n            runCatching {\n                smsLoginViewModel.sendSms(phoneNumberText.toLong()) { challenge: String, gt: String ->\n                    scope.launch(Dispatchers.Main) {\n                        setConfig(challenge, gt)\n                    }\n                }\n            }\n        }\n    }\n\n    val loginWithSms = {\n        keyboardController?.hide()\n        if (smsLoginViewModel.sendSmsState != SendSmsState.Success) {\n            R.string.sms_login_toast_send_sms_first.toast(context)\n        } else {\n            scope.launch(Dispatchers.IO) {\n                runCatching {\n                    smsLoginViewModel.loginWithSms(codeText.toInt()) {\n                        (context as Activity).finish()\n                    }\n                }\n            }\n        }\n    }\n\n    DisposableEffect(Unit) {\n        gt3GeetestUtils = GT3GeetestUtils(context)\n        gt3ConfigBean.apply {\n            pattern = 1\n            isCanceledOnTouchOutside = false\n            lang = null\n            timeout = 10000\n            webviewTimeout = 10000\n            corners = 24\n            listener = object : GT3Listener() {\n                override fun onReceiveCaptchaCode(p0: Int) {\n                    logger.info { \"Geetest - onReceiveCaptchaCode: $p0\" }\n                }\n\n                override fun onStatistics(p0: String?) {\n                    logger.info { \"Geetest - onStatistics: $p0\" }\n                }\n\n                override fun onClosed(p0: Int) {\n                    logger.info { \"Geetest - onClosed: $p0\" }\n                    smsLoginViewModel.clearCaptchaData()\n                }\n\n                override fun onSuccess(p0: String?) {\n                    logger.info { \"Geetest - onSuccess: $p0\" }\n                }\n\n                override fun onFailed(p0: GT3ErrorBean?) {\n                    logger.info { \"Geetest - onFailed: $p0\" }\n                    smsLoginViewModel.clearCaptchaData()\n                }\n\n                override fun onButtonClick() {\n                    logger.info { \"Geetest - onButtonClick\" }\n                }\n\n                override fun onDialogResult(result: String) {\n                    logger.info { \"Geetest - onDialogResult: $result\" }\n                    runCatching {\n                        val geetestResult = Json.decodeFromString<GeetestResult>(result)\n                        smsLoginViewModel.geetestChallenge = geetestResult.geetestChallenge\n                        smsLoginViewModel.geetestValidate = geetestResult.geetestValidate\n                        smsLoginViewModel.sendSmsState = SendSmsState.Ready\n                        gt3GeetestUtils?.showSuccessDialog()\n                        scope.launch(Dispatchers.IO) {\n                            smsLoginViewModel.sendSms(phoneNumberText.toLong()) { _, _ -> }\n                        }\n                    }.onFailure {\n                        gt3GeetestUtils?.showFailedDialog()\n                    }\n                }\n            }\n        }\n        gt3GeetestUtils!!.init(gt3ConfigBean)\n\n        onDispose {\n            gt3GeetestUtils?.destory()\n        }\n    }\n\n    Box(\n        modifier = modifier\n            .fillMaxSize(),\n        contentAlignment = Alignment.Center\n    ) {\n        Column(\n            horizontalAlignment = Alignment.Start,\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Row(\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                OutlinedTextField(\n                    value = phoneNumberText,\n                    onValueChange = {\n                        phoneNumberText = it\n                        // Clear captcha data when phone number changed\n                        smsLoginViewModel.clearCaptchaData()\n                    },\n                    label = { Text(text = stringResource(R.string.sms_login_phone_number)) },\n                    maxLines = 1,\n                    shape = MaterialTheme.shapes.medium,\n                    keyboardOptions = KeyboardOptions(\n                        keyboardType = KeyboardType.Phone,\n                        imeAction = ImeAction.Send\n                    ),\n                    keyboardActions = KeyboardActions(\n                        onSend = { sendSms() }\n                    )\n                )\n                Button(onClick = { sendSms() }) {\n                    Text(text = stringResource(R.string.sms_login_button_send_sms))\n                }\n            }\n            Row(\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                OutlinedTextField(\n                    value = codeText,\n                    onValueChange = { codeText = it },\n                    label = { Text(text = stringResource(R.string.sms_login_code)) },\n                    maxLines = 1,\n                    shape = MaterialTheme.shapes.medium,\n                    keyboardOptions = KeyboardOptions(\n                        keyboardType = KeyboardType.Number,\n                        imeAction = ImeAction.Done\n                    ),\n                    keyboardActions = KeyboardActions(\n                        onDone = {\n                            loginWithSms()\n                        }\n                    )\n                )\n                Button(onClick = { loginWithSms() }) {\n                    Text(text = stringResource(R.string.sms_login_button_login))\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/DrawerContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main\n\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.AccountCircle\nimport androidx.compose.material.icons.filled.Home\nimport androidx.compose.material.icons.filled.Movie\nimport androidx.compose.material.icons.filled.OndemandVideo\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material.icons.filled.Settings\nimport androidx.compose.material.icons.filled.Videocam\nimport androidx.compose.material3.NavigationRail\nimport androidx.compose.material3.NavigationRailItem\nimport androidx.compose.material3.NavigationRailItemDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.entity.NavSwitchMode\nimport dev.aaa1115910.bv.tv.util.drawerNavItemsFlow\nimport dev.aaa1115910.bv.tv.util.parseDrawerNavItemsOrder\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.isDpadRight\nimport dev.aaa1115910.bv.util.isKeyDown\nimport dev.aaa1115910.bv.util.onDelayFocusChanged\nimport kotlinx.coroutines.delay\n\n// 创建全局的FocusRequester映射表，方便外部使用\nval drawerItemFocusRequesters = mutableMapOf<DrawerItem, FocusRequester>().apply {\n    DrawerItem.entries.filter { it != DrawerItem.User && it != DrawerItem.Settings }\n        .forEach { item ->\n            this[item] = FocusRequester()\n        }\n}\n\n// 用于记住每个内容页当前选中的Tab\nval currentSelectedTabs = mutableStateMapOf<DrawerItem, Int>()\n\n@Composable\nfun DrawerContent(\n    modifier: Modifier = Modifier,\n    isLogin: Boolean = false,\n    avatar: String = \"\",\n    username: String = \"\",\n    navSwitchMode: NavSwitchMode = NavSwitchMode.Auto,\n    onDrawerItemChanged: (DrawerItem) -> Unit = {},\n    onDrawerItemfocused: (DrawerItem) -> Unit = {},\n    onOpenSettings: () -> Unit = {},\n    onShowUserPanel: () -> Unit = {},\n    onFocusToContent: () -> Unit = {},\n    onLogin: () -> Unit = {}\n) {\n    var selectedItem by remember { mutableStateOf(DrawerItem.Home) }\n    // 添加一个新的状态用于即时跟踪获得焦点的项目\n    var focusedItem by remember { mutableStateOf(DrawerItem.Home) }\n\n    var focusOnContent by remember { mutableStateOf(true) }\n    var tabMoved by remember { mutableStateOf(true) }\n\n    LaunchedEffect(selectedItem) {\n        tabMoved = false\n        delay(200)\n        onDrawerItemChanged(selectedItem)\n        // 别急着向右移动焦点，动画还没结束\n        delay(200)\n        tabMoved = true\n    }\n\n    LaunchedEffect(focusedItem) {\n        onDrawerItemfocused(focusedItem)\n    }\n\n    Column(\n        modifier = modifier\n            .fillMaxSize()\n            .padding(4.dp)\n            .onPreviewKeyEvent { keyEvent ->\n                if (keyEvent.isDpadRight()) {\n                    if (keyEvent.isKeyDown()) {\n                        if (tabMoved) {\n                            focusedItem = selectedItem\n                            onFocusToContent()\n                        }\n                        return@onPreviewKeyEvent true\n                    }\n                }\n                false\n            }\n            .onFocusChanged {\n                if (it.hasFocus) {\n                    drawerItemFocusRequesters[focusedItem]?.requestFocus()\n                }\n            }\n            .onDelayFocusChanged(delayTime = 0) {\n                focusOnContent = !it.hasFocus\n            },\n        verticalArrangement = Arrangement.SpaceBetween\n    ) {\n        NavigationRailItem(\n            modifier = Modifier\n                .onFocusChanged {\n                    if (it.hasFocus && !focusOnContent) {\n                        focusedItem = DrawerItem.User\n                    }\n                },\n            onClick = {\n                if (isLogin) {\n                    onShowUserPanel()\n                } else {\n                    onLogin()\n                }\n                focusedItem = DrawerItem.User\n            },\n            selected = focusedItem == DrawerItem.User,\n            colors = NavigationRailItemDefaults.colors(\n                selectedIconColor = Color.Transparent,\n                indicatorColor = Color.Transparent\n            ),\n            icon = {\n                if (isLogin) {\n                    AsyncImage(\n                        modifier = Modifier\n                            .size(52.dp)\n                            .ifElse(\n                                !focusOnContent && focusedItem == DrawerItem.User,\n                                Modifier\n                                    .border(\n                                        width = 2.dp,\n                                        color = MaterialTheme.colorScheme.inverseSurface,\n                                        shape = CircleShape\n                                    )\n                            )\n                            .padding(3.dp)  // 边框和图片之间的1dp透明区域\n                            .clip(CircleShape),\n                        model = avatar,\n                        contentDescription = null,\n                        contentScale = ContentScale.FillBounds\n                    )\n                } else {\n                    Icon(\n                        modifier = Modifier\n                            .size(46.dp)\n                            .ifElse(\n                                !focusOnContent && focusedItem == DrawerItem.User,\n                                Modifier\n                                    .border(\n                                        width = 2.dp,\n                                        color = MaterialTheme.colorScheme.inverseSurface,\n                                        shape = CircleShape\n                                    )\n                            )\n                            .clip(CircleShape),\n                        imageVector = DrawerItem.User.displayIcon,\n                        contentDescription = null,\n                        tint = if (!focusOnContent && focusedItem == DrawerItem.User) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.inverseSurface\n                    )\n                }\n            },\n            label = {\n                Text(\n                    modifier = Modifier.offset(y = (-3).dp),\n                    text = if (isLogin) username\n                    else DrawerItem.User.displayName,\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis,\n                    style = MaterialTheme.typography.bodySmall\n                )\n            }\n        )\n        // 菜单项列表：根据设置排序和过滤\n        val menuItems by drawerNavItemsFlow.collectAsState(\n            initial = remember { parseDrawerNavItemsOrder(Prefs.drawerNavItemsOrder) }\n        )\n\n        // 当选中项不在可见列表中时，自动切换到第一个可见项\n        LaunchedEffect(menuItems) {\n            if (menuItems.isNotEmpty() && selectedItem !in menuItems && selectedItem != DrawerItem.User && selectedItem != DrawerItem.Settings) {\n                selectedItem = menuItems.first()\n                focusedItem = menuItems.first()\n            }\n        }\n        \n        LazyColumn(\n            modifier = Modifier.weight(1f),\n            verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically)\n        ) {\n            items(menuItems.size) { index ->\n                val item = menuItems[index]\n                val isSelected = selectedItem == item\n                val isFocused = focusedItem == item && !focusOnContent\n                NavigationRailItem(\n                        modifier = Modifier\n                            .focusRequester(drawerItemFocusRequesters[item]!!)\n                            .onFocusChanged {\n                                if (it.hasFocus && !focusOnContent) {\n                                    focusedItem = item\n                                    if (navSwitchMode == NavSwitchMode.Auto) {\n                                        selectedItem = item\n                                    }\n                                }\n                            },\n                        onClick = {\n                            selectedItem = item\n                            focusedItem = item\n                        },\n                        selected = isSelected || isFocused,\n                        colors = NavigationRailItemDefaults.colors(\n                            indicatorColor = when {\n                                focusOnContent -> MaterialTheme.colorScheme.surfaceVariant\n                                isFocused && isSelected -> MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.75f)\n                                isFocused && !isSelected -> MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.3f)\n                                isSelected -> MaterialTheme.colorScheme.inverseSurface\n                                else -> MaterialTheme.colorScheme.surfaceVariant\n                            }\n                        ),\n                        icon = {\n                            Icon(\n                                imageVector = item.displayIcon,\n                                contentDescription = null,\n                                tint = when {\n                                    isFocused && !isSelected -> MaterialTheme.colorScheme.inverseSurface\n                                    !focusOnContent && isSelected -> MaterialTheme.colorScheme.surface\n                                    else -> MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.85f)\n                                }\n                            )\n                        },\n                        label = {\n                            Text(\n                                modifier = Modifier.offset(y = (-4).dp),\n                                text = item.displayName,\n                                style = MaterialTheme.typography.bodySmall\n                            )\n                        }\n                    )\n            }\n        }\n        NavigationRailItem(\n            modifier = Modifier.onFocusChanged {\n                if (it.hasFocus && !focusOnContent) {\n                    focusedItem = DrawerItem.Settings\n                }\n            },\n            onClick = {\n                onOpenSettings()\n                focusedItem = DrawerItem.Settings\n            },\n            selected = run {\n                val s = selectedItem == DrawerItem.Settings\n                val f = focusedItem == DrawerItem.Settings && !focusOnContent\n                s || f\n            },\n            colors = run {\n                val s = selectedItem == DrawerItem.Settings\n                val f = focusedItem == DrawerItem.Settings && !focusOnContent\n                NavigationRailItemDefaults.colors(\n                    indicatorColor = when {\n                        focusOnContent -> MaterialTheme.colorScheme.surfaceVariant\n                        f && s -> MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.75f)\n                        f && !s -> MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.3f)\n                        s -> MaterialTheme.colorScheme.inverseSurface\n                        else -> MaterialTheme.colorScheme.surfaceVariant\n                    }\n                )\n            },\n            icon = {\n                val s = selectedItem == DrawerItem.Settings\n                val f = focusedItem == DrawerItem.Settings && !focusOnContent\n                Icon(\n                    imageVector = DrawerItem.Settings.displayIcon,\n                    contentDescription = null,\n                    tint = when {\n                        f && !s -> MaterialTheme.colorScheme.inverseSurface\n                        !focusOnContent && s -> MaterialTheme.colorScheme.surface\n                        else -> MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.85f)\n                    }\n                )\n            },\n            label = {\n                Text(\n                    modifier = Modifier.offset(y = (-3).dp),\n                    text = DrawerItem.Settings.displayName,\n                    style = MaterialTheme.typography.bodySmall\n                )\n            }\n        )\n    }\n}\n\nenum class DrawerItem(\n    val displayName: String,\n    val displayIcon: ImageVector\n) {\n    User(displayName = \"点击登录\", displayIcon = Icons.Default.AccountCircle),\n    Search(displayName = \"搜索\", displayIcon = Icons.Default.Search),\n    Home(displayName = \"首页\", displayIcon = Icons.Default.Home),\n    UGC(displayName = \"UGC\", displayIcon = Icons.Default.OndemandVideo),\n    PGC(displayName = \"PGC\", displayIcon = Icons.Default.Movie),\n    Live(displayName = \"直播\", displayIcon = Icons.Default.Videocam),\n    Settings(displayName = \"设置\", displayIcon = Icons.Default.Settings), ;\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun DrawerContentPreview() {\n    BVTheme {\n        Box(\n            modifier = Modifier\n                .fillMaxHeight()\n                .width(180.dp)\n        ) {\n            NavigationRail(\n                modifier = Modifier\n                    .align(Alignment.CenterStart)\n                    .width(72.dp),\n                containerColor = MaterialTheme.colorScheme.inverseOnSurface\n            ) {\n                DrawerContent()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/HomeContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.tv.component.HomeTopNavItem\nimport dev.aaa1115910.bv.tv.component.TopNav\nimport dev.aaa1115910.bv.tv.screens.main.home.DynamicsScreen\nimport dev.aaa1115910.bv.tv.screens.main.home.PopularScreen\nimport dev.aaa1115910.bv.tv.screens.main.home.RecommendScreen\nimport dev.aaa1115910.bv.tv.screens.user.FavoriteScreen\nimport dev.aaa1115910.bv.tv.screens.user.FollowingSeasonScreen\nimport dev.aaa1115910.bv.tv.screens.user.HistoryScreen\nimport dev.aaa1115910.bv.tv.screens.user.ToViewScreen\nimport dev.aaa1115910.bv.tv.util.homeNavItemsFlow\nimport dev.aaa1115910.bv.tv.util.parseHomeNavItemsOrder\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.viewmodel.UserViewModel\nimport dev.aaa1115910.bv.viewmodel.home.DynamicViewModel\nimport dev.aaa1115910.bv.viewmodel.home.PopularViewModel\nimport dev.aaa1115910.bv.viewmodel.home.RecommendViewModel\nimport dev.aaa1115910.bv.viewmodel.user.FavoriteViewModel\nimport dev.aaa1115910.bv.viewmodel.user.FollowingSeasonViewModel\nimport dev.aaa1115910.bv.viewmodel.user.HistoryViewModel\nimport dev.aaa1115910.bv.viewmodel.user.ToViewViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun HomeContent(\n    modifier: Modifier = Modifier,\n    navFocusRequester: FocusRequester,\n    recommendViewModel: RecommendViewModel = koinViewModel(),\n    popularViewModel: PopularViewModel = koinViewModel(),\n    dynamicViewModel: DynamicViewModel = koinViewModel(),\n    favouriteViewModel: FavoriteViewModel = koinViewModel(),\n    followingSeasonViewModel: FollowingSeasonViewModel = koinViewModel(),\n    historyViewModel: HistoryViewModel = koinViewModel(),\n    toViewViewModel: ToViewViewModel = koinViewModel(),\n    userViewModel: UserViewModel = koinViewModel()\n) {\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger(\"HomeContent\")\n    val navSwitchMode by Prefs.navSwitchModeFlow.collectAsState(Prefs.navSwitchMode)\n\n    val recommendState = rememberLazyGridState()\n    val popularState = rememberLazyGridState()\n    val dynamicState = rememberLazyGridState()\n    val favoriteState = rememberLazyGridState()\n    val followingSeasonState = rememberLazyGridState()\n    val historyState = rememberLazyGridState()\n    val toViewState = rememberLazyGridState()\n    \n    var focusOnContent by remember { mutableStateOf(false) }\n    var topNavHasFocus by remember { mutableStateOf(false) }\n    \n    // 用于管理延迟加载的Job\n    var loadJob by remember { mutableStateOf<Job?>(null) }\n\n    // 根据设置获取过滤和排序后的导航项列表\n    val homeNavItems by homeNavItemsFlow.collectAsState(\n        initial = remember { parseHomeNavItemsOrder(Prefs.homeNavItemsOrder) }\n    )\n\n    // 处理空列表情况：如果所有导航项都被隐藏，强制显示推荐\n    val effectiveNavItems = if (homeNavItems.isEmpty()) {\n        listOf(HomeTopNavItem.Recommend)\n    } else {\n        homeNavItems\n    }\n\n    // 从全局状态获取上次选择的标签位置，如果没有则默认为Recommend\n    var selectedTab by remember {\n        mutableStateOf(\n            currentSelectedTabs[DrawerItem.Home]\n                ?.let { HomeTopNavItem.entries.getOrNull(it) }\n                ?: HomeTopNavItem.entries.getOrElse(Prefs.defaultHomeTab) { HomeTopNavItem.Recommend }\n        )\n    }\n\n    fun initData () {\n        scope.launch {\n            when (selectedTab) {\n                HomeTopNavItem.Recommend -> {\n                    if (recommendViewModel.recommendVideoList.isEmpty()) {\n                        recommendViewModel.loadMore()\n                    }\n                }\n\n                HomeTopNavItem.Popular -> {\n                    if (popularViewModel.popularVideoList.isEmpty()) {\n                        popularViewModel.loadMore()\n                    }\n                }\n\n                HomeTopNavItem.Dynamics -> {\n                    if (dynamicViewModel.dynamicVideoList.isEmpty()) {\n                        dynamicViewModel.loadMoreVideo()\n                    }\n                }\n\n                HomeTopNavItem.Favorite -> {\n//                    if (favouriteViewModel.favorites.isEmpty() && userViewModel.isLogin) {\n//                        favouriteViewModel.updateFoldersInfo()\n//                    }\n                }\n\n                HomeTopNavItem.FollowingSeason -> {\n//                    if (followingSeasonViewModel.followingSeasons.isEmpty() && userViewModel.isLogin) {\n//                        followingSeasonViewModel.loadMore()\n//                    }\n                }\n\n                HomeTopNavItem.History -> {\n//                    if (historyViewModel.histories.isEmpty() && userViewModel.isLogin) {\n//                        historyViewModel.update()\n//                    }\n                }\n\n                HomeTopNavItem.ToView -> {\n//                    if (toViewViewModel.histories.isEmpty() && userViewModel.isLogin) {\n//                        toViewViewModel.update()\n//                    }\n                }\n            }\n        }\n    }\n\n    // 当选中标签变化时，保存到全局状态并处理延迟加载\n    LaunchedEffect(selectedTab) {\n        currentSelectedTabs[DrawerItem.Home] = selectedTab.ordinal\n        \n        // 取消之前的延迟加载\n        loadJob?.cancel()\n        \n        // 开始新的延迟加载\n        loadJob = scope.launch(Dispatchers.IO) {\n            delay(300L)\n            initData()\n        }\n    }\n    val currentListOnTop by remember {\n        derivedStateOf {\n            with(\n                when (selectedTab) {\n                    HomeTopNavItem.Recommend -> recommendState\n                    HomeTopNavItem.Popular -> popularState\n                    HomeTopNavItem.Dynamics -> dynamicState\n                    HomeTopNavItem.Favorite -> favoriteState\n                    HomeTopNavItem.FollowingSeason -> followingSeasonState\n                    HomeTopNavItem.History -> historyState\n                    HomeTopNavItem.ToView -> toViewState\n                }\n            ) {\n                firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0\n            }\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        initData()\n    }\n\n    //监听登录变化\n    LaunchedEffect(userViewModel.isLogin) {\n        if (userViewModel.isLogin) {\n            //login\n            userViewModel.updateUserInfo()\n        } else {\n            //logout\n            userViewModel.clearUserInfo()\n        }\n    }\n\n    BackHandler(focusOnContent || topNavHasFocus) {\n        if (topNavHasFocus) {\n            drawerItemFocusRequesters[DrawerItem.Home]?.requestFocus()\n            return@BackHandler\n        }\n        navFocusRequester.requestFocus(scope)\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            TopNav(\n                modifier = Modifier\n                    .focusRequester(navFocusRequester)\n                    .onFocusChanged { topNavHasFocus = it.hasFocus },\n                items = effectiveNavItems,\n                initialSelectedItem = selectedTab,\n                navSwitchMode = navSwitchMode,\n                onSelectedChanged = { nav ->\n                    loadJob?.cancel()\n                    selectedTab = nav as HomeTopNavItem\n                },\n                onClick = { nav ->\n                    loadJob?.cancel()\n                    \n                    when (nav) {\n                        HomeTopNavItem.Recommend -> {\n                            logger.fInfo { \"clear recommend data\" }\n                            recommendViewModel.clearData()\n                            logger.fInfo { \"reload recommend data\" }\n                            scope.launch(Dispatchers.IO) { recommendViewModel.loadMore() }\n                        }\n\n                        HomeTopNavItem.Popular -> {\n                            logger.fInfo { \"clear popular data\" }\n                            popularViewModel.clearData()\n                            logger.fInfo { \"reload popular data\" }\n                            scope.launch(Dispatchers.IO) { popularViewModel.loadMore() }\n                        }\n\n                        HomeTopNavItem.Dynamics -> {\n                            logger.fInfo { \"clear dynamic data\" }\n                            dynamicViewModel.clearVideoData()\n                            logger.fInfo { \"reload dynamic data\" }\n                            scope.launch(Dispatchers.IO) { dynamicViewModel.loadMoreVideo() }\n                        }\n\n                        HomeTopNavItem.Favorite -> {\n                            if (userViewModel.isLogin) {\n                                favouriteViewModel.clearData()\n                                favouriteViewModel.updateFoldersInfo()\n                            }\n                        }\n\n                        HomeTopNavItem.FollowingSeason -> {\n                            if (userViewModel.isLogin) {\n                                followingSeasonViewModel.clearData()\n                                followingSeasonViewModel.loadMore()\n                            }\n                        }\n\n                        HomeTopNavItem.History -> {\n                            if (userViewModel.isLogin) {\n                                historyViewModel.clearData()\n                                historyViewModel.update()\n                            }\n                        }\n\n                        HomeTopNavItem.ToView -> {\n                            if (userViewModel.isLogin) {\n                                toViewViewModel.clearData()\n                                toViewViewModel.update()\n                            }\n                        }\n                    }\n                },\n                onLeftKeyEvent = {\n                    // 顶部栏最左侧按左键时，跳转到左侧导航栏\n                    drawerItemFocusRequesters[DrawerItem.Home]?.requestFocus()\n                }\n            )\n        }\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize()\n                .onFocusChanged { focusOnContent = it.hasFocus }\n        ) {\n            AnimatedContent(\n                targetState = selectedTab,\n                label = \"home animated content\",\n                transitionSpec = {\n                    val coefficient = 10\n                    val initialIndex = effectiveNavItems.indexOf(initialState)\n                        .takeIf { it >= 0 } ?: initialState.ordinal\n                    val targetIndex = effectiveNavItems.indexOf(targetState)\n                        .takeIf { it >= 0 } ?: targetState.ordinal\n                    if (targetIndex < initialIndex) {\n                        fadeIn() + slideInHorizontally { -it / coefficient } togetherWith\n                                fadeOut() + slideOutHorizontally { it / coefficient }\n                    } else {\n                        fadeIn() + slideInHorizontally { it / coefficient } togetherWith\n                                fadeOut() + slideOutHorizontally { -it / coefficient }\n                    }\n                }\n            ) { screen ->\n                when (screen) {\n                    HomeTopNavItem.Recommend -> RecommendScreen(lazyGridState = recommendState)\n                    HomeTopNavItem.Popular -> PopularScreen(lazyGridState = popularState)\n                    HomeTopNavItem.Dynamics -> {\n                        if (userViewModel.isLogin) {\n                            DynamicsScreen(lazyGridState = dynamicState)\n                        } else {\n                            LoginRequiredScreen()\n                        }\n                    }\n                    HomeTopNavItem.Favorite -> {\n                        if (userViewModel.isLogin) {\n                            FavoriteScreen(showPageTitle = false)\n                        } else {\n                            LoginRequiredScreen()\n                        }\n                    }\n                    HomeTopNavItem.FollowingSeason -> {\n                        if (userViewModel.isLogin) {\n                            FollowingSeasonScreen(\n                                showPageTitle = false,\n                                topTabFocusRequester = navFocusRequester\n                            )\n                        } else {\n                            LoginRequiredScreen()\n                        }\n                    }\n                    HomeTopNavItem.History -> {\n                        if (userViewModel.isLogin) {\n                            HistoryScreen(\n                                showPageTitle = false,\n                                topTabFocusRequester = navFocusRequester\n                            )\n                        } else {\n                            LoginRequiredScreen()\n                        }\n                    }\n                    HomeTopNavItem.ToView -> {\n                        if (userViewModel.isLogin) {\n                            ToViewScreen(\n                                showPageTitle = false,\n                                topTabFocusRequester = navFocusRequester\n                            )\n                        } else {\n                            LoginRequiredScreen()\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun LoginRequiredScreen() {\n    Box(\n        modifier = Modifier.fillMaxSize(),\n        contentAlignment = Alignment.Center\n    ) {\n        Text(text = \"请先登录\")\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/LiveContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main\n\nimport android.content.Context\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport dev.aaa1115910.biliapi.entity.live.LiveAreaItem\nimport dev.aaa1115910.bv.tv.activities.video.VideoPlayerV3Activity\nimport dev.aaa1115910.bv.tv.component.LoadingTip\nimport dev.aaa1115910.bv.tv.component.TopNav\nimport dev.aaa1115910.bv.tv.component.TopNavItem\nimport dev.aaa1115910.bv.tv.component.live.LiveRoomCard\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.getLiveNavItemAreaGroup\nimport dev.aaa1115910.bv.tv.util.isLiveAreaItem\nimport dev.aaa1115910.bv.tv.util.isLiveFollowingItem\nimport dev.aaa1115910.bv.tv.util.isLiveRecommendItem\nimport dev.aaa1115910.bv.tv.util.liveNavItemsOrderFlow\nimport dev.aaa1115910.bv.tv.util.parseLiveNavItemsOrder\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.live.LiveMode\nimport dev.aaa1115910.bv.viewmodel.live.LiveViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\nimport dev.aaa1115910.biliapi.entity.live.LiveAreaGroup\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\n\n// 子分区 TopNavItem\nprivate data class SubAreaNavItem(val area: LiveAreaItem) : TopNavItem {\n    override fun getDisplayName(context: Context): String = area.name\n}\n\n@Composable\nfun LiveContent(\n    modifier: Modifier = Modifier,\n    navFocusRequester: FocusRequester,\n    liveViewModel: LiveViewModel = koinViewModel()\n) {\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger(\"LiveContent\")\n    val context = LocalContext.current\n    val navSwitchMode by Prefs.navSwitchModeFlow.collectAsState(Prefs.navSwitchMode)\n\n    val gridState = rememberLazyGridState()\n    // 使用 MainScreen 传入的 FocusRequester 作为默认入口焦点（从侧边栏按右进入内容区）\n    val parentNavFocusRequester = navFocusRequester\n    val subNavFocusRequester = remember { FocusRequester() }\n    val roomListFocusRestorer = rememberTvLazyListFocusRestorer()\n    var focusOnContent by remember { mutableStateOf(false) }\n    var parentNavHasFocus by remember { mutableStateOf(false) }\n    var subNavHasFocus by remember { mutableStateOf(false) }\n\n    val currentListOnTop by remember {\n        derivedStateOf {\n            gridState.firstVisibleItemIndex == 0 && gridState.firstVisibleItemScrollOffset == 0\n        }\n    }\n\n    // 监听焦点位置，触发分页加载\n    val focusedIndex = liveViewModel.lastFocusedRoomIndex\n    val totalItems = liveViewModel.roomList.size\n    LaunchedEffect(focusedIndex, totalItems, liveViewModel.loading) {\n        if (totalItems > 0 && (totalItems < 10 || focusedIndex >= totalItems - 8) && liveViewModel.hasMore && !liveViewModel.loading) {\n            logger.info { \"Trigger load more, focusedIndex: $focusedIndex, totalItems: $totalItems\" }\n            liveViewModel.loadMore()\n        }\n    }\n\n    LaunchedEffect(liveViewModel.roomList, liveViewModel.loading) {\n        if (liveViewModel.roomList.isEmpty() && liveViewModel.loading) {\n            liveViewModel.lastFocusedRoomIndex = 0\n            gridState.scrollToItem(0)\n        }\n    }\n\n    BackHandler(focusOnContent || subNavHasFocus || parentNavHasFocus) {\n        logger.info { \"onFocusBackToNav\" }\n        if (subNavHasFocus) {\n            parentNavFocusRequester.requestFocus(scope)\n            return@BackHandler\n        }\n        if (parentNavHasFocus) {\n            drawerItemFocusRequesters[DrawerItem.Live]?.requestFocus()\n            return@BackHandler\n        }\n        // 推荐/关注模式没有子分区栏，直接返回主分区栏\n        if (liveViewModel.currentMode == LiveMode.AREA) {\n            subNavFocusRequester.requestFocus(scope)\n        } else {\n            parentNavFocusRequester.requestFocus(scope)\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            androidx.compose.foundation.layout.Column {\n                // 第一行：推荐 + 关注 + 主分区（根据设置过滤和排序）\n                val liveNavOrderString by liveNavItemsOrderFlow.collectAsState(\n                    initial = Prefs.liveNavItemsOrder\n                )\n\n                val parentNavItems = remember(\n                    liveNavOrderString,\n                    liveViewModel.parentAreaGroups.size,\n                    liveViewModel.isLoggedIn\n                ) {\n                    val items = parseLiveNavItemsOrder(\n                        liveNavOrderString,\n                        liveViewModel.parentAreaGroups,\n                        liveViewModel.isLoggedIn\n                    )\n                    // 全部隐藏时强制显示推荐\n                    items.ifEmpty {\n                        parseLiveNavItemsOrder(\"\", emptyList(), false)\n                    }\n                }\n\n                val currentParentVisible = remember(\n                    parentNavItems,\n                    liveViewModel.areaGroupsLoadCompleted,\n                    liveViewModel.currentMode,\n                    liveViewModel.currentParentGroup?.id\n                ) {\n                    liveViewModel.areaGroupsLoadCompleted && parentNavItems.any {\n                        it.matchesLiveMode(liveViewModel.currentMode, liveViewModel.currentParentGroup?.id)\n                    }\n                }\n\n                // 首次加载或配置变化时，确保 ViewModel 模式与导航列表一致\n                var initialSynced by remember { mutableStateOf(false) }\n                LaunchedEffect(\n                    parentNavItems,\n                    liveViewModel.areaGroupsLoadCompleted,\n                    liveViewModel.currentMode,\n                    liveViewModel.currentParentGroup?.id\n                ) {\n                    if (parentNavItems.isEmpty() || !liveViewModel.areaGroupsLoadCompleted) {\n                        return@LaunchedEffect\n                    }\n\n                    val shouldSwitch = if (!initialSynced) {\n                        initialSynced = true\n                        // 首次：排序后的第一项与默认模式不一致时切换\n                        !parentNavItems.first().matchesLiveMode(liveViewModel.currentMode, liveViewModel.currentParentGroup?.id)\n                    } else {\n                        // 后续：当前选中项被隐藏时切换\n                        !currentParentVisible\n                    }\n\n                    if (shouldSwitch) {\n                        liveViewModel.lastFocusedRoomIndex = 0\n                        parentNavItems.first().applyToLiveViewModel(liveViewModel)\n                        gridState.scrollToItem(0)\n                        return@LaunchedEffect\n                    }\n\n                    if (currentParentVisible) {\n                        liveViewModel.ensureRoomsLoaded()\n                    }\n                }\n\n                val initialSelectedParent = remember(liveViewModel.currentMode, liveViewModel.currentParentGroup, parentNavItems) {\n                    parentNavItems.firstOrNull {\n                        it.matchesLiveMode(liveViewModel.currentMode, liveViewModel.currentParentGroup?.id)\n                    } ?: parentNavItems.firstOrNull()\n                }\n\n                if (parentNavItems.isNotEmpty()) {\n                    TopNav(\n                        modifier = Modifier\n                            .focusRequester(parentNavFocusRequester)\n                            .onFocusChanged { parentNavHasFocus = it.hasFocus },\n                        items = parentNavItems,\n                        initialSelectedItem = initialSelectedParent,\n                        navSwitchMode = navSwitchMode,\n                        onSelectedChanged = { nav ->\n                            liveViewModel.lastFocusedRoomIndex = 0\n                            scope.launch { gridState.scrollToItem(0) }\n                            nav.applyToLiveViewModel(liveViewModel)\n                        },\n                        onClick = { nav ->\n                            if (nav.matchesLiveMode(liveViewModel.currentMode, liveViewModel.currentParentGroup?.id)) {\n                                liveViewModel.lastFocusedRoomIndex = 0\n                                liveViewModel.refresh()\n                                scope.launch { gridState.scrollToItem(0) }\n                            }\n                        },\n                        onLeftKeyEvent = {\n                            drawerItemFocusRequesters[DrawerItem.Live]?.requestFocus()\n                        }\n                    )\n                }\n\n                // 第二行：子分区（仅在分区模式下显示）\n                if (liveViewModel.currentMode == LiveMode.AREA && liveViewModel.subAreaList.isNotEmpty()) {\n                    // 监听 currentParentGroup 变化以触发子分区列表更新\n                    val subNavItems = remember(liveViewModel.currentParentGroup, liveViewModel.subAreaList.size) {\n                        liveViewModel.subAreaList.map { SubAreaNavItem(it) }\n                    }\n                    TopNav(\n                        modifier = Modifier\n                            .focusRequester(subNavFocusRequester)\n                            .onFocusChanged { subNavHasFocus = it.hasFocus },\n                        paddingTop = 0.dp,\n                        items = subNavItems,\n                        useSmallSize = true,\n                        initialSelectedItem = subNavItems.firstOrNull { it.area.id == liveViewModel.currentSubArea?.id },\n                        navSwitchMode = navSwitchMode,\n                        onSelectedChanged = { nav ->\n                            (nav as? SubAreaNavItem)?.let {\n                                liveViewModel.lastFocusedRoomIndex = 0\n                                liveViewModel.switchSubArea(it.area)\n                                scope.launch { gridState.scrollToItem(0) }\n                            }\n                        },\n                        onClick = { nav ->\n                            (nav as? SubAreaNavItem)?.let { item ->\n                                if (item.area.id == liveViewModel.currentSubArea?.id) {\n                                    liveViewModel.lastFocusedRoomIndex = 0\n                                    liveViewModel.refresh()\n                                    scope.launch { gridState.scrollToItem(0) }\n                                }\n                            }\n                        },\n                        onLeftKeyEvent = {\n                            parentNavFocusRequester.requestFocus(scope)\n                        }\n                    )\n                }\n            }\n        }\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(innerPadding)\n                .onFocusChanged { focusOnContent = it.hasFocus }\n        ) {\n            if (liveViewModel.roomList.isEmpty() && liveViewModel.loading) {\n                Row(\n                    modifier = Modifier.align(Alignment.Center)\n                ){\n                    LoadingTip()\n                }\n            } else {\n                ProvideListBringIntoViewSpec(topPadding = 12.dp, bottomPadding = 28.dp) {\n                    LazyVerticalGrid(\n                        modifier = roomListFocusRestorer.containerModifier(\n                            Modifier\n                                .fillMaxSize()\n                                .blockDownFocusExitAtGridEnd(\n                                    currentIndex = focusedIndex,\n                                    itemCount = totalItems,\n                                    columnCount = 4\n                                )\n                        ),\n                        state = gridState,\n                        columns = GridCells.Fixed(4),\n                        contentPadding = PaddingValues(20.dp, 0.dp, 20.dp, 20.dp),\n                        verticalArrangement = Arrangement.spacedBy(13.dp),\n                        horizontalArrangement = Arrangement.spacedBy(13.dp)\n                    ) {\n                        itemsIndexed(\n                            items = liveViewModel.roomList,\n                            key = { index, room -> \"$index-room-${room.roomId}\" }\n                        ) { index, room ->\n                            val entryCardModifier = roomListFocusRestorer.firstItemModifier(index)\n\n                            LiveRoomCard(\n                                modifier = entryCardModifier,\n                                data = room,\n                                onClick = {\n                                    // 保存焦点位置\n                                    liveViewModel.lastFocusedRoomIndex = index\n                                    if (room.liveStatus != 1) {\n                                        \"${room.uname} 未开播\".toast(context)\n                                        return@LiveRoomCard\n                                    }\n                                    // 启动播放器\n                                    VideoPlayerV3Activity.actionStartLive(\n                                        context = context,\n                                        roomId = room.roomId,\n                                        title = room.title,\n                                        upId = room.uid,\n                                        upName = room.uname,\n                                        upFace = room.face,\n                                        watchedNum = room.watchedShow?.num ?: (room.online / 10)\n                                    )\n                                },\n                                onFocus = {\n                                    liveViewModel.lastFocusedRoomIndex = index\n                                    logger.debug { \"Focus on room ${room.roomId}\" }\n                                }\n                            )\n                        }\n\n                        // 加载中提示\n                        if (liveViewModel.loading) {\n                            item {\n                                LoadingTip()\n                            }\n                        }\n\n                        // 没有更多了\n                        if (!liveViewModel.hasMore) {\n                            item(span = { GridItemSpan(maxLineSpan) }) {\n                                Row(\n                                    modifier = Modifier.offset(y = (-16).dp),\n                                    horizontalArrangement = Arrangement.Center\n                                ) {\n                                    Text(\n                                        text = \"没有更多内容了~\",\n                                        style = MaterialTheme.typography.bodyMedium,\n                                        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\nprivate fun TopNavItem.matchesLiveMode(mode: LiveMode, parentGroupId: Int?): Boolean = when {\n    isLiveRecommendItem(this) -> mode == LiveMode.RECOMMEND\n    isLiveFollowingItem(this) -> mode == LiveMode.FOLLOWING\n    isLiveAreaItem(this) -> mode == LiveMode.AREA && getLiveNavItemAreaGroup(this)?.id == parentGroupId\n    else -> false\n}\n\nprivate fun TopNavItem.applyToLiveViewModel(viewModel: LiveViewModel) {\n    when {\n        isLiveRecommendItem(this) -> viewModel.switchToRecommend()\n        isLiveFollowingItem(this) -> viewModel.switchToFollowing()\n        isLiveAreaItem(this) -> getLiveNavItemAreaGroup(this)?.let { viewModel.switchParentArea(it) }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/PgcContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.tv.component.PgcTopNavItem\nimport dev.aaa1115910.bv.tv.component.TopNav\nimport dev.aaa1115910.bv.tv.screens.main.pgc.AnimeContent\nimport dev.aaa1115910.bv.tv.screens.main.pgc.DocumentaryContent\nimport dev.aaa1115910.bv.tv.screens.main.pgc.GuoChuangContent\nimport dev.aaa1115910.bv.tv.screens.main.pgc.MovieContent\nimport dev.aaa1115910.bv.tv.screens.main.pgc.TvContent\nimport dev.aaa1115910.bv.tv.screens.main.pgc.VarietyContent\nimport dev.aaa1115910.bv.tv.util.parsePgcTopNavItemsOrder\nimport dev.aaa1115910.bv.tv.util.pgcNavItemsFlow\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.util.rememberDebouncer\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcAnimeViewModel\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcDocumentaryViewModel\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcGuoChuangViewModel\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcMovieViewModel\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcTvViewModel\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcVarietyViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun PgcContent(\n    modifier: Modifier = Modifier,\n    navFocusRequester: FocusRequester,\n    pgcAnimeViewModel: PgcAnimeViewModel = koinViewModel(),\n    pgcGuoChuangViewModel: PgcGuoChuangViewModel = koinViewModel(),\n    pgcMovieViewModel: PgcMovieViewModel = koinViewModel(),\n    pgcDocumentaryViewModel: PgcDocumentaryViewModel = koinViewModel(),\n    pgcTvViewModel: PgcTvViewModel = koinViewModel(),\n    pgcVarietyViewModel: PgcVarietyViewModel = koinViewModel()\n) {\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger(\"PgcContent\")\n    val navSwitchMode by Prefs.navSwitchModeFlow.collectAsState(Prefs.navSwitchMode)\n\n    val animeState = rememberLazyListState()\n    val guoChuangState = rememberLazyListState()\n    val movieState = rememberLazyListState()\n    val documentaryState = rememberLazyListState()\n    val tvState = rememberLazyListState()\n    val varietyState = rememberLazyListState()\n    \n    var focusOnContent by remember { mutableStateOf(false) }\n    var topNavHasFocus by remember { mutableStateOf(false) }\n\n    // 根据设置获取过滤和排序后的导航项列表\n    val pgcNavItems by pgcNavItemsFlow.collectAsState(\n        initial = remember { parsePgcTopNavItemsOrder(Prefs.pgcNavItemsOrder) }\n    )\n\n    // 处理空列表情况：如果全部被隐藏，强制显示第一个\n    val effectiveNavItems = if (pgcNavItems.isEmpty()) {\n        listOf(PgcTopNavItem.entries.first())\n    } else {\n        pgcNavItems\n    }\n\n    // 使用remember的key参数确保只有在DrawerItem.PGC的tab状态变化时才重新计算\n    var selectedTab by remember {\n        mutableStateOf(\n            currentSelectedTabs[DrawerItem.PGC]\n                ?.let { PgcTopNavItem.entries.getOrNull(it) }\n                ?: effectiveNavItems.first()\n        )\n    }\n\n    // 如果当前选中项被隐藏，则自动切换到第一个可见项\n    LaunchedEffect(effectiveNavItems) {\n        if (selectedTab !in effectiveNavItems) {\n            selectedTab = effectiveNavItems.first()\n        }\n    }\n\n    // 当选中标签变化时，保存到全局状态\n    LaunchedEffect(selectedTab) {\n        currentSelectedTabs[DrawerItem.PGC] = selectedTab.ordinal\n    }\n\n    val currentListOnTop by remember {\n        derivedStateOf {\n            with(\n                when (selectedTab) {\n                    PgcTopNavItem.Anime -> animeState\n                    PgcTopNavItem.GuoChuang -> guoChuangState\n                    PgcTopNavItem.Movie -> movieState\n                    PgcTopNavItem.Documentary -> documentaryState\n                    PgcTopNavItem.Tv -> tvState\n                    PgcTopNavItem.Variety -> varietyState\n                }\n            ) {\n                firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0\n            }\n        }\n    }\n\n    //启动时加载当前选中tab的数据\n    LaunchedEffect(selectedTab) {\n        when (selectedTab) {\n            PgcTopNavItem.Anime -> {\n                if (pgcAnimeViewModel.feedItems.isEmpty()) {\n                    logger.fInfo { \"加载动画数据\" }\n                    pgcAnimeViewModel.init()\n                }\n            }\n            PgcTopNavItem.GuoChuang -> {\n                if (pgcGuoChuangViewModel.feedItems.isEmpty()) {\n                    logger.fInfo { \"加载国创数据\" }\n                    pgcGuoChuangViewModel.init()\n                }\n            }\n            PgcTopNavItem.Movie -> {\n                if (pgcMovieViewModel.feedItems.isEmpty()) {\n                    logger.fInfo { \"加载电影数据\" }\n                    pgcMovieViewModel.init()\n                }\n            }\n            PgcTopNavItem.Documentary -> {\n                if (pgcDocumentaryViewModel.feedItems.isEmpty()) {\n                    logger.fInfo { \"加载纪录片数据\" }\n                    pgcDocumentaryViewModel.init()\n                }\n            }\n            PgcTopNavItem.Tv -> {\n                if (pgcTvViewModel.feedItems.isEmpty()) {\n                    logger.fInfo { \"加载电视剧数据\" }\n                    pgcTvViewModel.init()\n                }\n            }\n            PgcTopNavItem.Variety -> {\n                if (pgcVarietyViewModel.feedItems.isEmpty()) {\n                    logger.fInfo { \"加载综艺数据\" }\n                    pgcVarietyViewModel.init()\n                }\n            }\n        }\n    }\n\n    BackHandler(focusOnContent || topNavHasFocus) {\n        logger.fInfo { \"onFocusBackToNav\" }\n        // 如果顶部导航有焦点，则返回到左边栏的PGC位置\n        if (topNavHasFocus) {\n            drawerItemFocusRequesters[DrawerItem.PGC]?.requestFocus()\n            return@BackHandler\n        }\n        navFocusRequester.requestFocus(scope)\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            TopNav(\n                modifier = Modifier\n                    .focusRequester(navFocusRequester)\n                    .onFocusChanged { topNavHasFocus = it.hasFocus },\n                items = effectiveNavItems,\n                initialSelectedItem = selectedTab,\n                navSwitchMode = navSwitchMode,\n                onSelectedChanged = { nav ->\n                    selectedTab = nav as PgcTopNavItem\n                },\n                onClick = { nav ->\n                    when (nav) {\n                        PgcTopNavItem.Anime -> pgcAnimeViewModel.reloadAll()\n                        PgcTopNavItem.GuoChuang -> pgcGuoChuangViewModel.reloadAll()\n                        PgcTopNavItem.Movie -> pgcMovieViewModel.reloadAll()\n                        PgcTopNavItem.Documentary -> pgcDocumentaryViewModel.reloadAll()\n                        PgcTopNavItem.Tv -> pgcTvViewModel.reloadAll()\n                        PgcTopNavItem.Variety -> pgcVarietyViewModel.reloadAll()\n                    }\n                },\n                onLeftKeyEvent = {\n                    // 顶部栏最左侧按左键时，跳转到左侧导航栏\n                    drawerItemFocusRequesters[DrawerItem.PGC]?.requestFocus()\n                }\n            )\n        }\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize()\n                .onFocusChanged { focusOnContent = it.hasFocus }\n        ) {\n            AnimatedContent(\n                targetState = selectedTab,\n                label = \"pgc animated content\",\n                transitionSpec = {\n                    val coefficient = 10\n                    val initialIndex = effectiveNavItems.indexOf(initialState)\n                        .takeIf { it >= 0 } ?: initialState.ordinal\n                    val targetIndex = effectiveNavItems.indexOf(targetState)\n                        .takeIf { it >= 0 } ?: targetState.ordinal\n                    if (targetIndex < initialIndex) {\n                        fadeIn() + slideInHorizontally { -it / coefficient } togetherWith\n                                fadeOut() + slideOutHorizontally { it / coefficient }\n                    } else {\n                        fadeIn() + slideInHorizontally { it / coefficient } togetherWith\n                                fadeOut() + slideOutHorizontally { -it / coefficient }\n                    }\n                }\n            ) { screen ->\n                when (screen) {\n                    PgcTopNavItem.Anime -> AnimeContent(lazyListState = animeState)\n                    PgcTopNavItem.GuoChuang -> GuoChuangContent(lazyListState = guoChuangState)\n                    PgcTopNavItem.Movie -> MovieContent(lazyListState = movieState)\n                    PgcTopNavItem.Documentary -> DocumentaryContent(lazyListState = documentaryState)\n                    PgcTopNavItem.Tv -> TvContent(lazyListState = tvState)\n                    PgcTopNavItem.Variety -> VarietyContent(lazyListState = varietyState)\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/UgcContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport dev.aaa1115910.bv.tv.component.TopNav\nimport dev.aaa1115910.bv.tv.component.UgcTopNavItem\nimport dev.aaa1115910.bv.tv.screens.main.ugc.CreateUgcContent\nimport dev.aaa1115910.bv.tv.util.parseUgcTopNavItemsOrder\nimport dev.aaa1115910.bv.tv.util.ugcNavItemsFlow\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcAiViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcAnimalViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcCarViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcCinephileViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcDanceViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcDougaViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcEmotionViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcEntViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcFashionViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcFoodViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcGameViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcGymViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcHandmakeViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcHealthViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcHomeViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcInformationViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcKichikuViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcKnowledgeViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcLifeExperienceViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcLifeJoyViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcMusicViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcMysticismViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcOutdoorsViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcPaintingViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcParentingViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcRuralViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcShortplayViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcSportsViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcTechViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcTravelViewModel\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcVlogViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun UgcContent(\n    modifier: Modifier = Modifier,\n    navFocusRequester: FocusRequester,\n    ugcDougaViewModel: UgcDougaViewModel = koinViewModel(),\n    ugcGameViewModel: UgcGameViewModel = koinViewModel(),\n    ugcKichikuViewModel: UgcKichikuViewModel = koinViewModel(),\n    ugcMusicViewModel: UgcMusicViewModel = koinViewModel(),\n    ugcDanceViewModel: UgcDanceViewModel = koinViewModel(),\n    ugcCinephileViewModel: UgcCinephileViewModel = koinViewModel(),\n    ugcEntViewModel: UgcEntViewModel = koinViewModel(),\n    ugcKnowledgeViewModel: UgcKnowledgeViewModel = koinViewModel(),\n    ugcTechViewModel: UgcTechViewModel = koinViewModel(),\n    ugcInformationViewModel: UgcInformationViewModel = koinViewModel(),\n    ugcFoodViewModel: UgcFoodViewModel = koinViewModel(),\n    ugcShortplayViewModel: UgcShortplayViewModel = koinViewModel(),\n    ugcCarViewModel: UgcCarViewModel = koinViewModel(),\n    ugcFashionViewModel: UgcFashionViewModel = koinViewModel(),\n    ugcSportsViewModel: UgcSportsViewModel = koinViewModel(),\n    ugcAnimalViewModel: UgcAnimalViewModel = koinViewModel(),\n    ugcVlogViewModel: UgcVlogViewModel = koinViewModel(),\n    ugcPaintingViewModel: UgcPaintingViewModel = koinViewModel(),\n    ugcAiViewModel: UgcAiViewModel = koinViewModel(),\n    ugcHomeViewModel: UgcHomeViewModel = koinViewModel(),\n    ugcOutdoorsViewModel: UgcOutdoorsViewModel = koinViewModel(),\n    ugcGymViewModel: UgcGymViewModel = koinViewModel(),\n    ugcHandmakeViewModel: UgcHandmakeViewModel = koinViewModel(),\n    ugcTravelViewModel: UgcTravelViewModel = koinViewModel(),\n    ugcRuralViewModel: UgcRuralViewModel = koinViewModel(),\n    ugcParentingViewModel: UgcParentingViewModel = koinViewModel(),\n    ugcHealthViewModel: UgcHealthViewModel = koinViewModel(),\n    ugcEmotionViewModel: UgcEmotionViewModel = koinViewModel(),\n    ugcLifeJoyViewModel: UgcLifeJoyViewModel = koinViewModel(),\n    ugcLifeExperienceViewModel: UgcLifeExperienceViewModel = koinViewModel(),\n    ugcMysticismViewModel: UgcMysticismViewModel = koinViewModel(),\n) {\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger(\"UgcContent\")\n    val navSwitchMode by Prefs.navSwitchModeFlow.collectAsState(Prefs.navSwitchMode)\n\n    // 为当前选中的tab创建LazyGridState\n    val currentLazyGridState = rememberLazyGridState()\n    var focusOnContent by remember { mutableStateOf(false) }\n    var topNavHasFocus by remember { mutableStateOf(false) }\n\n    // 根据设置获取过滤和排序后的导航项列表\n    val ugcNavItems by ugcNavItemsFlow.collectAsState(\n        initial = remember { parseUgcTopNavItemsOrder(Prefs.ugcNavItemsOrder) }\n    )\n\n    // 处理空列表情况：如果全部被隐藏，强制显示第一个\n    val effectiveNavItems = if (ugcNavItems.isEmpty()) {\n        listOf(UgcTopNavItem.entries.first())\n    } else {\n        ugcNavItems\n    }\n\n    // 使用remember的key参数确保只有在DrawerItem.UGC的tab状态变化时才重新计算\n    var selectedTab by remember {\n        mutableStateOf(\n            currentSelectedTabs[DrawerItem.UGC]\n                ?.let { UgcTopNavItem.entries.getOrNull(it) }\n                ?: effectiveNavItems.first()\n        )\n    }\n\n    // 如果当前选中项被隐藏，则自动切换到第一个可见项\n    LaunchedEffect(effectiveNavItems) {\n        if (selectedTab !in effectiveNavItems) {\n            selectedTab = effectiveNavItems.first()\n        }\n    }\n\n    // 获取所有ViewModels的映射\n    val viewModelMap = remember {\n        mapOf(\n            UgcTopNavItem.Douga to ugcDougaViewModel,\n            UgcTopNavItem.Game to ugcGameViewModel,\n            UgcTopNavItem.Kichiku to ugcKichikuViewModel,\n            UgcTopNavItem.Music to ugcMusicViewModel,\n            UgcTopNavItem.Dance to ugcDanceViewModel,\n            UgcTopNavItem.Cinephile to ugcCinephileViewModel,\n            UgcTopNavItem.Ent to ugcEntViewModel,\n            UgcTopNavItem.Knowledge to ugcKnowledgeViewModel,\n            UgcTopNavItem.Tech to ugcTechViewModel,\n            UgcTopNavItem.Information to ugcInformationViewModel,\n            UgcTopNavItem.Food to ugcFoodViewModel,\n            UgcTopNavItem.ShortPlay to ugcShortplayViewModel,\n            UgcTopNavItem.Car to ugcCarViewModel,\n            UgcTopNavItem.Fashion to ugcFashionViewModel,\n            UgcTopNavItem.Sports to ugcSportsViewModel,\n            UgcTopNavItem.Animal to ugcAnimalViewModel,\n            UgcTopNavItem.Vlog to ugcVlogViewModel,\n            UgcTopNavItem.Painting to ugcPaintingViewModel,\n            UgcTopNavItem.Ai to ugcAiViewModel,\n            UgcTopNavItem.Home to ugcHomeViewModel,\n            UgcTopNavItem.Outdoors to ugcOutdoorsViewModel,\n            UgcTopNavItem.Gym to ugcGymViewModel,\n            UgcTopNavItem.Handmake to ugcHandmakeViewModel,\n            UgcTopNavItem.Travel to ugcTravelViewModel,\n            UgcTopNavItem.Rural to ugcRuralViewModel,\n            UgcTopNavItem.Parenting to ugcParentingViewModel,\n            UgcTopNavItem.Health to ugcHealthViewModel,\n            UgcTopNavItem.Emotion to ugcEmotionViewModel,\n            UgcTopNavItem.LifeJoy to ugcLifeJoyViewModel,\n            UgcTopNavItem.LifeExperience to ugcLifeExperienceViewModel,\n            UgcTopNavItem.Mysticism to ugcMysticismViewModel\n        )\n    }\n\n    // 当选中标签变化时，保存到全局状态并处理懒加载\n    LaunchedEffect(selectedTab) {\n        currentSelectedTabs[DrawerItem.UGC] = selectedTab.ordinal\n\n        // 取消所有其他ViewModel的延迟加载\n        viewModelMap.values.forEach { viewModel ->\n            viewModel.cancelDelayedLoad()\n        }\n\n        // 为当前选中的ViewModel开始延迟加载\n        viewModelMap[selectedTab]?.loadDataWithDelay(300L)\n    }\n\n    BackHandler(focusOnContent || topNavHasFocus) {\n        logger.fInfo { \"onFocusBackToNav\" }\n        if (topNavHasFocus) {\n            drawerItemFocusRequesters[DrawerItem.UGC]?.requestFocus()\n            return@BackHandler\n        }\n        navFocusRequester.requestFocus(scope)\n        // 滚动到顶部（如果需要的话）\n        // scope.launch(Dispatchers.Main) {\n        //     currentLazyGridState.animateScrollToItem(0)\n        // }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            TopNav(\n                modifier = Modifier\n                    .focusRequester(navFocusRequester)\n                    .onFocusChanged { topNavHasFocus = it.hasFocus },\n                items = effectiveNavItems,\n                initialSelectedItem = selectedTab,\n                navSwitchMode = navSwitchMode,\n                onSelectedChanged = { nav ->\n                    selectedTab = nav as UgcTopNavItem\n                    // 取消非selectedTab的所有延迟加载\n                    viewModelMap\n                        .filterKeys { it != selectedTab }\n                        .values\n                        .forEach { it.cancelDelayedLoad() }\n\n                },\n                onClick = { nav ->\n                    // 点击时立即加载数据\n                    viewModelMap[nav as UgcTopNavItem]?.reloadAll()\n                },\n                onLeftKeyEvent = {\n                    // 顶部栏最左侧按左键时，跳转到左侧导航栏\n                    drawerItemFocusRequesters[DrawerItem.UGC]?.requestFocus()\n                }\n            )\n        }\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize()\n                .onFocusChanged { focusOnContent = it.hasFocus }\n        ) {\n            AnimatedContent(\n                targetState = selectedTab,\n                label = \"ugc animated content\",\n                transitionSpec = {\n                    val coefficient = 10\n                    val initialIndex = effectiveNavItems.indexOf(initialState)\n                        .takeIf { it >= 0 } ?: initialState.ordinal\n                    val targetIndex = effectiveNavItems.indexOf(targetState)\n                        .takeIf { it >= 0 } ?: targetState.ordinal\n                    if (targetIndex < initialIndex) {\n                        fadeIn() + slideInHorizontally { -it / coefficient } togetherWith\n                                fadeOut() + slideOutHorizontally { it / coefficient }\n                    } else {\n                        fadeIn() + slideInHorizontally { it / coefficient } togetherWith\n                                fadeOut() + slideOutHorizontally { -it / coefficient }\n                    }\n                }\n            ) { screen ->\n                CreateUgcContent(\n                    navItem = screen,\n                    lazyGridState = currentLazyGridState,\n                    ugcViewModel = viewModelMap[screen]!!\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/home/DynamicsScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.home\n\nimport android.content.Intent\nimport android.view.KeyEvent\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.user.DynamicVideo\nimport dev.aaa1115910.bv.R as SharedR\nimport dev.aaa1115910.bv.tv.component.LoadingTip\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.R\nimport dev.aaa1115910.bv.tv.activities.user.FollowActivity\nimport dev.aaa1115910.bv.tv.activities.video.SeasonInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport dev.aaa1115910.bv.viewmodel.home.DynamicViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.koinInject\n\n@Composable\nfun DynamicsScreen(\n    modifier: Modifier = Modifier,\n    lazyGridState: LazyGridState = rememberLazyGridState(),\n    dynamicViewModel: DynamicViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val videoInfoRepository: VideoInfoRepository = koinInject()\n    val listFocusRestorer = rememberTvLazyListFocusRestorer()\n    var currentFocusedIndex by remember { mutableIntStateOf(-1) }\n    val shouldLoadMore by remember {\n        derivedStateOf { dynamicViewModel.dynamicVideoList.isNotEmpty() && currentFocusedIndex + 12 > dynamicViewModel.dynamicVideoList.size }\n    }\n    val onClickVideo: (DynamicVideo) -> Unit = { dynamic ->\n        val proxyArea = ProxyArea.checkProxyArea(dynamic.title)\n        val hasSeasonHint = dynamic.seasonId != null || dynamic.epid != null\n\n        videoInfoRepository.preloadedVideoList.clear()\n        videoInfoRepository.preloadedVideoList.addAll(\n            dynamicViewModel.dynamicVideoList.map { item ->\n                VideoCardData(\n                    avid = item.aid,\n                    title = item.title,\n                    cover = item.cover,\n                    upName = item.author,\n                    upId = item.authorId,\n                    play = item.play,\n                    danmaku = item.danmaku,\n                    time = item.duration * 1000L,\n                    pubTime = item.pubTime\n                )\n            }\n        )\n\n        if (hasSeasonHint) {\n            SeasonInfoActivity.actionStart(\n                context = context,\n                epId = dynamic.epid,\n                seasonId = dynamic.seasonId,\n                proxyArea = proxyArea\n            )\n        } else {\n            VideoInfoActivity.actionStart(\n                context = context,\n                aid = dynamic.aid,\n                proxyArea = proxyArea\n            )\n        }\n    }\n\n    val onLongClickVideo: (DynamicVideo) -> Unit = { dynamic ->\n        UpInfoActivity.actionStart(\n            context,\n            mid = dynamic.authorId,\n            name = dynamic.author,\n            face = dynamic.authorFace\n        )\n    }\n\n    //不能直接使用 LaunchedEffect(currentFocusedIndex)，会导致整个页面重组\n    LaunchedEffect(shouldLoadMore) {\n        if (shouldLoadMore) {\n            scope.launch(Dispatchers.IO) {\n                dynamicViewModel.loadMoreVideo()\n            }\n        }\n    }\n\n    if (dynamicViewModel.isLogin) {\n        val padding = dimensionResource(R.dimen.grid_padding)\n        val spacedBy = dimensionResource(R.dimen.grid_spacedBy)\n        Text(\n            modifier = Modifier.fillMaxWidth().offset(x = (-20).dp, y = (-8).dp),\n            text = stringResource(R.string.entry_follow_screen),\n            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),\n            fontSize = 11.sp,\n            textAlign = TextAlign.End\n        )\n        ProvideListBringIntoViewSpec {\n            LazyVerticalGrid(\n                modifier = listFocusRestorer.containerModifier(modifier.fillMaxSize())\n                    .blockDownFocusExitAtGridEnd(\n                        currentIndex = currentFocusedIndex,\n                        itemCount = dynamicViewModel.dynamicVideoList.size,\n                        columnCount = 4\n                    )\n                    .onFocusChanged{\n                        if (!it.isFocused) {\n                            currentFocusedIndex = -1\n                        }\n                    }\n                    .onPreviewKeyEvent { keyEvent ->\n                        if (\n                            keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_UP &&\n                            keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_MENU\n                        ) {\n                            context.startActivity(Intent(context, FollowActivity::class.java))\n                            return@onPreviewKeyEvent true\n                        }\n                        false\n                    },\n                columns = GridCells.Fixed(4),\n                state = lazyGridState,\n                contentPadding = PaddingValues(padding),\n                verticalArrangement = Arrangement.spacedBy(spacedBy),\n                horizontalArrangement = Arrangement.spacedBy(spacedBy)\n            ) {\n                itemsIndexed(\n                    items = dynamicViewModel.dynamicVideoList,\n                    key = { index, item -> \"$index-av-${item.aid}\" }\n                ) { index, item ->\n                    SmallVideoCard(\n                        modifier = listFocusRestorer.firstItemModifier(index),\n                        data = remember(item.aid) {\n                            VideoCardData(\n                                avid = item.aid,\n                                title = item.title,\n                                cover = item.cover,\n                                play = item.play,\n                                danmaku = item.danmaku,\n                                upName = item.author,\n                                time = item.duration * 1000L,\n                                pubTime = item.pubTime,\n                                isChargingArc = item.isChargingArc,\n                                badgeText = item.chargingArcBadge\n                            )\n                        },\n                        onClick = { onClickVideo(item) },\n                        onLongClick = {onLongClickVideo(item) },\n                        onFocus = { currentFocusedIndex = index }\n                    )\n                }\n\n                if (\n                    dynamicViewModel.dynamicVideoList.isEmpty() &&\n                    !dynamicViewModel.loadingVideo &&\n                    !dynamicViewModel.videoHasMore\n                ) {\n                    item(span = { GridItemSpan(maxLineSpan) }) {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center\n                        ) {\n                            Text(\n                                text = stringResource(SharedR.string.no_data),\n                                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                            )\n                        }\n                    }\n                }\n\n                if (dynamicViewModel.loadingVideo) {\n                    item(span = { GridItemSpan(maxLineSpan) }) {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center\n                        ) {\n                            LoadingTip()\n                        }\n                    }\n                }\n\n                if (!dynamicViewModel.videoHasMore && dynamicViewModel.dynamicVideoList.isNotEmpty()) {\n                    item(span = { GridItemSpan(maxLineSpan) }) {\n                        Text(\n                            text = \"没有更多了捏\",\n                            color = Color.White\n                        )\n                    }\n                }\n            }\n        }\n    } else {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(text = \"请先登录\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/home/PopularScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.home\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\nimport dev.aaa1115910.bv.tv.component.LoadingTip\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.tv.R\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport dev.aaa1115910.bv.viewmodel.home.PopularViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.koinInject\n\n@Composable\nfun PopularScreen(\n    modifier: Modifier = Modifier,\n    lazyGridState: LazyGridState = rememberLazyGridState(),\n    popularViewModel: PopularViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val videoInfoRepository: VideoInfoRepository = koinInject()\n    val listFocusRestorer = rememberTvLazyListFocusRestorer()\n    var currentFocusedIndex by remember { mutableIntStateOf(0) }\n    val shouldLoadMore by remember {\n        derivedStateOf { popularViewModel.popularVideoList.isNotEmpty() && currentFocusedIndex + 12 > popularViewModel.popularVideoList.size }\n    }\n\n    val onClickVideo: (UgcItem) -> Unit = { ugcItem ->\n        videoInfoRepository.preloadedVideoList.clear()\n        videoInfoRepository.preloadedVideoList.addAll(\n            popularViewModel.popularVideoList.map { item ->\n                VideoCardData(\n                    avid = item.aid,\n                    title = item.title,\n                    cover = item.cover,\n                    upName = item.author,\n                    upId = item.authorId,\n                    play = if (item.play == -1L) null else item.play,\n                    danmaku = if (item.danmaku == -1) null else item.danmaku,\n                    time = item.duration * 1000L,\n                    pubTime = item.pubTime\n                )\n            }\n        )\n        VideoInfoActivity.actionStart(context, ugcItem.aid)\n    }\n\n    val onLongClickVideo: (UgcItem) -> Unit = { ugcItem ->\n        UpInfoActivity.actionStart(\n            context,\n            mid = ugcItem.authorId,\n            name = ugcItem.author,\n            face = ugcItem.authorFace\n        )\n    }\n\n    LaunchedEffect(shouldLoadMore) {\n        if (shouldLoadMore) {\n            scope.launch(Dispatchers.IO) {\n                popularViewModel.loadMore()\n            }\n        }\n    }\n\n    val padding = dimensionResource(R.dimen.grid_padding)\n    val spacedBy = dimensionResource(R.dimen.grid_spacedBy)\n    ProvideListBringIntoViewSpec {\n        LazyVerticalGrid(\n            modifier = listFocusRestorer.containerModifier(\n                modifier\n                    .fillMaxSize()\n                    .blockDownFocusExitAtGridEnd(\n                        currentIndex = currentFocusedIndex,\n                        itemCount = popularViewModel.popularVideoList.size,\n                        columnCount = 4\n                    )\n            ),\n            columns = GridCells.Fixed(4),\n            state = lazyGridState,\n            contentPadding = PaddingValues(padding),\n            verticalArrangement = Arrangement.spacedBy(spacedBy),\n            horizontalArrangement = Arrangement.spacedBy(spacedBy)\n        ) {\n            itemsIndexed(\n                items = popularViewModel.popularVideoList,\n                key = { index, item -> \"$index-av-${item.aid}\" }\n            ) { index, item ->\n                SmallVideoCard(\n                    modifier = listFocusRestorer.firstItemModifier(index),\n                    data = remember(item.aid) {\n                        VideoCardData(\n                            avid = item.aid,\n                            title = item.title,\n                            cover = item.cover,\n                            play = item.play,\n                            danmaku = item.danmaku,\n                            upName = item.author,\n                            time = item.duration * 1000L,\n                            pubTime = item.pubTime\n                        )\n                    },\n                    onClick = { onClickVideo(item) },\n                    onLongClick = {onLongClickVideo(item) },\n                    onFocus = { currentFocusedIndex = index }\n                )\n            }\n\n            if (popularViewModel.loading) {\n                item(span = { GridItemSpan(maxLineSpan) }) {\n                    Box(\n                        modifier = Modifier.fillMaxSize(),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        LoadingTip()\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/home/RecommendScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.home\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\n\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\nimport dev.aaa1115910.bv.tv.component.LoadingTip\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.tv.R\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport dev.aaa1115910.bv.viewmodel.home.RecommendViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.koinInject\n\n@Composable\nfun RecommendScreen(\n    modifier: Modifier = Modifier,\n    lazyGridState: LazyGridState = rememberLazyGridState(),\n    recommendViewModel: RecommendViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val videoInfoRepository: VideoInfoRepository = koinInject()\n    val listFocusRestorer = rememberTvLazyListFocusRestorer()\n    var currentFocusedIndex by remember { mutableIntStateOf(0) }\n    val shouldLoadMore by remember {\n        derivedStateOf { recommendViewModel.recommendVideoList.isNotEmpty() && currentFocusedIndex + 12 > recommendViewModel.recommendVideoList.size }\n    }\n\n    val onClickVideo: (UgcItem) -> Unit = { ugcItem ->\n        videoInfoRepository.preloadedVideoList.clear()\n        videoInfoRepository.preloadedVideoList.addAll(\n            recommendViewModel.recommendVideoList.map { item ->\n                VideoCardData(\n                    avid = item.aid,\n                    title = item.title,\n                    cover = item.cover,\n                    upName = item.author,\n                    upId = item.authorId,\n                    play = if (item.play == -1L) null else item.play,\n                    danmaku = if (item.danmaku == -1) null else item.danmaku,\n                    time = item.duration * 1000L,\n                    pubTime = item.pubTime\n                )\n            }\n        )\n        VideoInfoActivity.actionStart(context, ugcItem.aid)\n    }\n\n    val onLongClickVideo: (UgcItem) -> Unit = { ugcItem ->\n        UpInfoActivity.actionStart(\n            context,\n            mid = ugcItem.authorId,\n            name = ugcItem.author,\n            face = ugcItem.authorFace\n        )\n    }\n\n    //不能直接使用 LaunchedEffect(currentFocusedIndex)，会导致整个页面重组\n    LaunchedEffect(shouldLoadMore) {\n        if (shouldLoadMore) {\n            scope.launch(Dispatchers.IO) {\n                recommendViewModel.loadMore()\n            }\n        }\n    }\n\n    val padding = dimensionResource(R.dimen.grid_padding)\n    val spacedBy = dimensionResource(R.dimen.grid_spacedBy)\n    ProvideListBringIntoViewSpec {\n        LazyVerticalGrid(\n            modifier = listFocusRestorer.containerModifier(\n                modifier\n                    .fillMaxSize()\n                    .blockDownFocusExitAtGridEnd(\n                        currentIndex = currentFocusedIndex,\n                        itemCount = recommendViewModel.recommendVideoList.size,\n                        columnCount = 4\n                    )\n            ),\n            columns = GridCells.Fixed(4),\n            state = lazyGridState,\n            contentPadding = PaddingValues(padding),\n            verticalArrangement = Arrangement.spacedBy(spacedBy),\n            horizontalArrangement = Arrangement.spacedBy(spacedBy)\n        ) {\n            itemsIndexed(\n                items = recommendViewModel.recommendVideoList,\n                key = { index, item -> \"$index-av-${item.aid}\" }\n            ) { index, item ->\n                SmallVideoCard(\n                    modifier = listFocusRestorer.firstItemModifier(index),\n                    data = remember(item.aid) {\n                        VideoCardData(\n                            avid = item.aid,\n                            title = item.title,\n                            cover = item.cover,\n                            play = with(item.play) { if (this == -1L) null else this },\n                            danmaku = with(item.danmaku) { if (this == -1) null else this },\n                            upName = item.author,\n                            time = item.duration * 1000L,\n                            pubTime = item.pubTime\n                        )\n                    },\n                    onClick = { onClickVideo(item) },\n                    onLongClick = {onLongClickVideo(item) },\n                    onFocus = { currentFocusedIndex = index }\n                )\n            }\n\n            if (recommendViewModel.loading) {\n                item(span = { GridItemSpan(maxLineSpan) }) {\n                    Box(\n                        modifier = Modifier.fillMaxSize(),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        LoadingTip()\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/pgc/AnimeContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.pgc\n\nimport android.content.Intent\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.rounded.List\nimport androidx.compose.material.icons.rounded.Alarm\nimport androidx.compose.material.icons.rounded.Favorite\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.activities.pgc.PgcIndexActivity\nimport dev.aaa1115910.bv.tv.activities.pgc.anime.AnimeTimelineActivity\nimport dev.aaa1115910.bv.tv.activities.user.FollowingSeasonActivity\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcAnimeViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun AnimeContent(\n    modifier: Modifier = Modifier,\n    lazyListState: LazyListState,\n    pgcViewModel: PgcAnimeViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n\n    val onOpenTimeline: () -> Unit = {\n        context.startActivity(Intent(context, AnimeTimelineActivity::class.java))\n    }\n    val onOpenFollowing: () -> Unit = {\n        context.startActivity(Intent(context, FollowingSeasonActivity::class.java))\n    }\n    val onOpenIndex: () -> Unit = {\n        PgcIndexActivity.actionStart(context = context, pgcType = PgcType.Anime)\n    }\n    val onOpenGamerAni: () -> Unit = {\n        val packageManager = context.packageManager\n        val gamerAniPackageName = \"tw.com.gamer.android.animad\"\n        packageManager.getLeanbackLaunchIntentForPackage(gamerAniPackageName)?.let {\n            context.startActivity(it)\n        } ?: run {\n            R.string.anime_home_button_gamer_ani_launch_failed.toast(context)\n        }\n    }\n\n    PgcScaffold(\n        lazyListState = lazyListState,\n        pgcViewModel = pgcViewModel,\n        pgcType = PgcType.Anime,\n        featureButtons = {\n            AnimeFeatureButtons(\n                modifier = Modifier.padding(vertical = 24.dp),\n                onOpenTimeline = onOpenTimeline,\n                onOpenFollowing = onOpenFollowing,\n                onOpenIndex = onOpenIndex,\n                onOpenGamerAni = onOpenGamerAni\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun AnimeFeatureButtons(\n    modifier: Modifier = Modifier,\n    onOpenTimeline: () -> Unit,\n    onOpenFollowing: () -> Unit,\n    onOpenIndex: () -> Unit,\n    onOpenGamerAni: () -> Unit = {}\n) {\n    val buttons = listOf(\n        Triple(\n            stringResource(R.string.anime_home_button_timeline),\n            Icons.Rounded.Alarm,\n            onOpenTimeline\n        ),\n        Triple(\n            stringResource(R.string.anime_home_button_following),\n            Icons.Rounded.Favorite,\n            onOpenFollowing\n        ),\n        Triple(\n            stringResource(R.string.anime_home_button_index),\n            Icons.AutoMirrored.Rounded.List,\n            onOpenIndex\n        ),\n        Triple(\n            stringResource(R.string.anime_home_button_gamer_ani),\n            painterResource(R.drawable.ic_gamer_ani),\n            onOpenGamerAni\n        )\n    )\n    PgcFeatureButtons(\n        modifier = modifier,\n        buttons = buttons\n    )\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun AnimeFeatureButtonsPreview() {\n    BVTheme {\n        AnimeFeatureButtons(\n            modifier = Modifier,\n            onOpenTimeline = {},\n            onOpenFollowing = {},\n            onOpenIndex = {},\n            onOpenGamerAni = {}\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/pgc/DocumentaryContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.pgc\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.rounded.List\nimport androidx.compose.material.icons.rounded.QuestionMark\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.activities.pgc.PgcIndexActivity\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcDocumentaryViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun DocumentaryContent(\n    modifier: Modifier = Modifier,\n    lazyListState: LazyListState,\n    pgcViewModel: PgcDocumentaryViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n\n    val onOpenIndex: () -> Unit = {\n        PgcIndexActivity.actionStart(context = context, pgcType = PgcType.Documentary)\n    }\n\n    PgcScaffold(\n        lazyListState = lazyListState,\n        pgcViewModel = pgcViewModel,\n        pgcType = PgcType.Documentary,\n        featureButtons = {\n            DocumentaryFeatureButtons(\n                modifier = Modifier.padding(vertical = 24.dp),\n                onOpenIndex = onOpenIndex\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun DocumentaryFeatureButtons(\n    modifier: Modifier = Modifier,\n    onOpenIndex: () -> Unit\n) {\n    val buttons = listOf(\n        Triple(\n            stringResource(R.string.anime_home_button_index),\n            Icons.AutoMirrored.Rounded.List,\n            onOpenIndex\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        )\n    )\n    PgcFeatureButtons(\n        modifier = modifier,\n        buttons = buttons\n    )\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun DocumentaryFeatureButtonsPreview() {\n    BVTheme {\n        DocumentaryFeatureButtons(\n            modifier = Modifier,\n            onOpenIndex = {},\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/pgc/GuoChuangContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.pgc\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.rounded.List\nimport androidx.compose.material.icons.rounded.QuestionMark\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.activities.pgc.PgcIndexActivity\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcGuoChuangViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun GuoChuangContent(\n    modifier: Modifier = Modifier,\n    lazyListState: LazyListState,\n    pgcViewModel: PgcGuoChuangViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n\n    val onOpenIndex: () -> Unit = {\n        PgcIndexActivity.actionStart(context = context, pgcType = PgcType.GuoChuang)\n    }\n\n    PgcScaffold(\n        lazyListState = lazyListState,\n        pgcViewModel = pgcViewModel,\n        pgcType = PgcType.GuoChuang,\n        featureButtons = {\n            GuoChuangFeatureButtons(\n                modifier = Modifier.padding(vertical = 24.dp),\n                onOpenIndex = onOpenIndex\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun GuoChuangFeatureButtons(\n    modifier: Modifier = Modifier,\n    onOpenIndex: () -> Unit\n) {\n    val buttons = listOf(\n        Triple(\n            stringResource(R.string.anime_home_button_index),\n            Icons.AutoMirrored.Rounded.List,\n            onOpenIndex\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        )\n    )\n    PgcFeatureButtons(\n        modifier = modifier,\n        buttons = buttons\n    )\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun GuoChuangFeatureButtonsPreview() {\n    BVTheme {\n        GuoChuangFeatureButtons(\n            modifier = Modifier,\n            onOpenIndex = {},\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/pgc/MovieContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.pgc\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.rounded.List\nimport androidx.compose.material.icons.rounded.QuestionMark\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.activities.pgc.PgcIndexActivity\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcMovieViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun MovieContent(\n    modifier: Modifier = Modifier,\n    lazyListState: LazyListState,\n    pgcViewModel: PgcMovieViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n\n    val onOpenIndex: () -> Unit = {\n        PgcIndexActivity.actionStart(context = context, pgcType = PgcType.Movie)\n    }\n\n    PgcScaffold(\n        lazyListState = lazyListState,\n        pgcViewModel = pgcViewModel,\n        pgcType = PgcType.Movie,\n        featureButtons = {\n            MovieFeatureButtons(\n                modifier = Modifier.padding(vertical = 24.dp),\n                onOpenIndex = onOpenIndex\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun MovieFeatureButtons(\n    modifier: Modifier = Modifier,\n    onOpenIndex: () -> Unit\n) {\n    val buttons = listOf(\n        Triple(\n            stringResource(R.string.anime_home_button_index),\n            Icons.AutoMirrored.Rounded.List,\n            onOpenIndex\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        )\n    )\n    PgcFeatureButtons(\n        modifier = modifier,\n        buttons = buttons\n    )\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun MovieFeatureButtonsPreview() {\n    BVTheme {\n        MovieFeatureButtons(\n            modifier = Modifier,\n            onOpenIndex = {},\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/pgc/PgcCommon.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.pgc\n\nimport android.view.KeyEvent\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.BlendMode\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.pgc.PgcFeedData\nimport dev.aaa1115910.biliapi.entity.pgc.PgcItem\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.http.SeasonIndexType\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.tv.component.PgcCarousel\nimport dev.aaa1115910.bv.tv.component.videocard.SeasonCard\nimport dev.aaa1115910.bv.entity.carddata.SeasonCardData\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.activities.video.SeasonInfoActivity\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.resizedImageUrl\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.pgc.FeedListType\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcViewModel\n\n@Composable\nfun PgcScaffold(\n    modifier: Modifier = Modifier,\n    lazyListState: LazyListState,\n    pgcViewModel: PgcViewModel,\n    pgcType: PgcType,\n    featureButtons: (@Composable () -> Unit)? = null\n) {\n    val context = LocalContext.current\n    val carouselFocusRequester = remember { FocusRequester() }\n    val carouselFocusRestorer = rememberTvLazyListFocusRestorer(carouselFocusRequester)\n    val currentFeedIndex = remember { mutableIntStateOf(0) }\n\n    val carouselItems = pgcViewModel.carouselItems\n    val pgcFeeds = pgcViewModel.feedItems\n\n    ProvideListBringIntoViewSpec {\n        LazyColumn(\n            modifier = carouselFocusRestorer.containerModifier(\n                modifier\n                    .fillMaxSize()\n                    .blockDownFocusExitAtGridEnd(\n                        currentIndex = currentFeedIndex.intValue,\n                        itemCount = pgcFeeds.size,\n                        columnCount = 1\n                    )\n            ),\n            state = lazyListState\n        ) {\n            item {\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .horizontalScroll(rememberScrollState()),\n                    horizontalArrangement = Arrangement.Center\n                ) {\n                    PgcCarousel(\n                        modifier = Modifier\n                            .width(880.dp)\n                            .padding(32.dp, 0.dp)\n                            .focusRequester(carouselFocusRequester),\n                        data = carouselItems,\n                        onClick = { item ->\n                            SeasonInfoActivity.actionStart(\n                                context = context,\n                                epId = item.episodeId,\n                                seasonId = item.seasonId,\n                                proxyArea = ProxyArea.checkProxyArea(item.title)\n                            )\n                        }\n                    )\n                }\n            }\n            if (featureButtons != null) {\n                item {\n                    featureButtons()\n                }\n            } else {\n                item {\n                    Spacer(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .height(24.dp)\n                    )\n                }\n            }\n            itemsIndexed(\n                items = pgcFeeds,\n                key = { index, feedListItem ->\n                    when (feedListItem.type) {\n                        FeedListType.Ep -> \"$index-ep-${feedListItem.items?.firstOrNull()?.seasonId ?: index}-${feedListItem.items?.size ?: 0}\"\n                        FeedListType.Rank -> \"$index-rank-${feedListItem.rank?.title ?: index}\"\n                    }\n                }\n            ) { index, feedListItem ->\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(vertical = 12.dp)\n                        .onFocusChanged {\n                            if (it.hasFocus) {\n                                currentFeedIndex.intValue = index\n                                if (index + 10 > pgcFeeds.size) {\n                                    pgcViewModel.loadMore()\n                                }\n                            }\n                        },\n                    contentAlignment = Alignment.Center\n                ) {\n                    when (feedListItem.type) {\n                        FeedListType.Ep -> PgcFeedVideoRow(\n                            data = feedListItem.items!!\n                        )\n\n                        FeedListType.Rank -> PgcFeedRankRow(\n                            data = feedListItem.rank!!\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun PgcFeedVideoRow(\n    modifier: Modifier = Modifier,\n    data: List<PgcItem>\n) {\n    val context = LocalContext.current\n    val listFocusRestorer = rememberTvLazyListFocusRestorer()\n    LazyRow(\n        modifier = listFocusRestorer.containerModifier(modifier),\n        contentPadding = PaddingValues(horizontal = 24.dp),\n        horizontalArrangement = Arrangement.spacedBy(32.dp)\n    ) {\n        itemsIndexed(\n            items = data,\n            key = { index, feedItem -> \"$index-season-${feedItem.seasonId}\" }\n        ) { index, feedItem ->\n            val cardModifier = if (index == data.lastIndex) {\n                Modifier.onPreviewKeyEvent { keyEvent ->\n                    when (keyEvent.nativeKeyEvent.keyCode) {\n                        KeyEvent.KEYCODE_DPAD_RIGHT -> return@onPreviewKeyEvent true\n                    }\n                    false\n                }\n            } else {\n                Modifier\n            }\n\n            SeasonCard(\n                modifier = listFocusRestorer.firstItemModifier(index, cardModifier),\n                coverHeight = 180.dp,\n                data = SeasonCardData(\n                    seasonId = feedItem.seasonId,\n                    title = feedItem.title,\n                    subTitle = feedItem.subTitle,\n                    cover = feedItem.cover.resizedImageUrl(ImageSize.SeasonCoverThumbnail),\n                    rating = feedItem.rating\n                ),\n                onClick = {\n                    SeasonInfoActivity.actionStart(\n                        context = context,\n                        seasonId = feedItem.seasonId,\n                        proxyArea = ProxyArea.checkProxyArea(feedItem.title)\n                    )\n                }\n            )\n        }\n    }\n}\n\n@Composable\nfun PgcFeedRankRow(\n    modifier: Modifier = Modifier,\n    data: PgcFeedData.FeedRank\n) {\n    val context = LocalContext.current\n    val listFocusRestorer = rememberTvLazyListFocusRestorer()\n    Box(\n        modifier = modifier\n            .height(300.dp)\n    ) {\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .background(\n                    Brush.verticalGradient(\n                        colors = listOf(\n                            // light theme color: Color(250, 222, 214)\n                            Color(20, 18, 17),\n                            Color(20, 18, 17).copy(alpha = 0.298f)\n                        )\n                    )\n                )\n        ) {}\n        BoxWithConstraints {\n            AsyncImage(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .offset(x = (-1 * (0.25 * 1.6 * this.maxHeight.value)).dp)\n                    .graphicsLayer { alpha = 0.99f }\n                    .drawWithContent {\n                        val colors = listOf(\n                            Color.Black,\n                            Color.Transparent\n                        )\n                        drawContent()\n                        drawRect(\n                            brush = Brush.horizontalGradient(colors),\n                            blendMode = BlendMode.DstIn\n                        )\n                        drawRect(\n                            brush = Brush.verticalGradient(colors),\n                            blendMode = BlendMode.DstIn\n                        )\n                    },\n                model = data.cover,\n                contentDescription = null,\n                contentScale = ContentScale.FillHeight,\n                alpha = 1f\n            )\n        }\n        Row(\n            modifier = Modifier\n                .fillMaxHeight(),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Column(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .width(240.dp)\n                    .padding(32.dp),\n                verticalArrangement = Arrangement.Bottom,\n                horizontalAlignment = Alignment.End\n            ) {\n                Text(\n                    text = data.title,\n                    style = MaterialTheme.typography.titleLarge,\n                    color = Color.White\n                )\n                Text(\n                    text = data.subTitle,\n                    style = MaterialTheme.typography.bodySmall,\n                    color = Color.White.copy(alpha = 0.6f)\n                )\n            }\n\n            LazyRow(\n                modifier = listFocusRestorer.containerModifier(modifier),\n                contentPadding = PaddingValues(horizontal = 32.dp),\n                horizontalArrangement = Arrangement.spacedBy(18.dp)\n            ) {\n                itemsIndexed(\n                    items = data.items,\n                    key = { index, feedItem -> \"$index-season-${feedItem.seasonId}\" }\n                ) { index, feedItem ->\n                    val cardModifier = if (index == data.items.lastIndex) {\n                        Modifier.onPreviewKeyEvent {\n                            when (it.nativeKeyEvent.keyCode) {\n                                KeyEvent.KEYCODE_DPAD_RIGHT -> return@onPreviewKeyEvent true\n                            }\n                            false\n                        }\n                    } else {\n                        Modifier\n                    }\n\n                    SeasonCard(\n                        modifier = listFocusRestorer.firstItemModifier(index, cardModifier),\n                        coverHeight = 180.dp,\n                        data = SeasonCardData(\n                            seasonId = feedItem.seasonId,\n                            title = feedItem.title,\n                            subTitle = feedItem.subTitle,\n                            cover = feedItem.cover.resizedImageUrl(ImageSize.SeasonCoverThumbnail),\n                            rating = feedItem.rating\n                        ),\n                        onClick = {\n                            SeasonInfoActivity.actionStart(\n                                context = context,\n                                seasonId = feedItem.seasonId,\n                                proxyArea = ProxyArea.checkProxyArea(feedItem.title)\n                            )\n                        }\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nfun PgcFeedRankRowPreview() {\n    val data = PgcFeedData.FeedRank(\n        cover = \"http://i0.hdslb.com/bfs/archive/aae451dabf64ead2e983f92be76039a8ba233ade.png\",\n        title = \"热门热血番剧榜\",\n        subTitle = \"每小时更新\",\n        items = List(8) {\n            PgcItem(\n                cover = \"https://i0.hdslb.com/bfs/bangumi/image/f610305ad3922bee9d51748ab38da0c54e785b44.png\",\n                title = \"解雇后走上人生巅峰\",\n                subTitle = \"被解雇的暗黑士兵慢生活的第二人生\",\n                episodeId = 0,\n                seasonId = 0,\n                seasonType = SeasonIndexType.Anime,\n                rating = \"9.8\"\n            )\n        }\n    )\n    BVTheme {\n        PgcFeedRankRow(data = data)\n    }\n}\n\n@Composable\nfun PgcFeatureButtons(\n    modifier: Modifier = Modifier,\n    buttons: List<Triple<String, Any, () -> Unit>>\n) {\n    val buttonWidth = 185.dp\n\n    LazyRow(\n        modifier = modifier\n            .fillMaxWidth()\n            .height(80.dp),\n        horizontalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterHorizontally),\n        contentPadding = PaddingValues(horizontal = 32.dp)\n    ) {\n        itemsIndexed(\n            items = buttons,\n            key = { index, (title, _, _) -> \"$index-feature-$title\" }\n        ) { _, (title, icon, onClick) ->\n            when (icon) {\n                is ImageVector -> PgcFeatureButton(\n                    modifier = Modifier.width(buttonWidth),\n                    title = title,\n                    icon = icon,\n                    onClick = { onClick.invoke() }\n                )\n\n                is Painter -> PgcFeatureButton(\n                    modifier = Modifier.width(buttonWidth),\n                    title = title,\n                    icon = icon,\n                    onClick = { onClick.invoke() }\n                )\n\n                else -> {}\n            }\n        }\n    }\n}\n\n\n@Composable\nfun PgcFeatureButton(\n    modifier: Modifier = Modifier,\n    title: String,\n    icon: ImageVector,\n    onClick: () -> Unit\n) {\n    Surface(\n        modifier = modifier,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surfaceVariant,\n            focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n            pressedContainerColor = MaterialTheme.colorScheme.inverseSurface\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        onClick = onClick\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                Icon(imageVector = icon, contentDescription = null)\n                Text(\n                    text = title,\n                    style = MaterialTheme.typography.titleLarge\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun PgcFeatureButton(\n    modifier: Modifier = Modifier,\n    title: String,\n    icon: Painter,\n    onClick: () -> Unit\n) {\n    Surface(\n        modifier = modifier,\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surfaceVariant,\n            focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n            pressedContainerColor = MaterialTheme.colorScheme.inverseSurface\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        onClick = onClick\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                Icon(\n                    modifier = Modifier.size(24.dp),\n                    painter = icon,\n                    contentDescription = null\n                )\n                Text(\n                    text = title,\n                    style = MaterialTheme.typography.titleLarge\n                )\n            }\n        }\n    }\n}\n\nval showPlaceholderToast: () -> Unit = {\n    \"都说了介个是占位按钮了\".toast(BVApp.context)\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/pgc/PgcIndexScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.pgc\n\nimport android.app.Activity\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.component.pgc.IndexFilter\nimport dev.aaa1115910.bv.tv.component.videocard.SeasonCard\nimport dev.aaa1115910.bv.entity.carddata.SeasonCardData\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.activities.video.SeasonInfoActivity\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.getDisplayName\nimport dev.aaa1115910.bv.viewmodel.index.PgcIndexViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun PgcIndexScreen(\n    modifier: Modifier = Modifier,\n    pgcIndexViewModel: PgcIndexViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger { }\n    val gridFocusRestorer = rememberTvLazyListFocusRestorer()\n\n    var currentSeasonIndex by remember { mutableIntStateOf(0) }\n    val showLargeTitle by remember {\n        derivedStateOf {\n            currentSeasonIndex < 6\n        }\n    }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n\n    val pgcItems = pgcIndexViewModel.indexResultItems\n    val noMore = pgcIndexViewModel.noMore\n    var showFilter by remember { mutableStateOf(false) }\n    val filterReady by remember { derivedStateOf { pgcIndexViewModel.isFilterReady } }\n    val filterSignature by remember { derivedStateOf { pgcIndexViewModel.filterSignature } }\n    val filterSections = pgcIndexViewModel.filterSections\n    val selectedFilters = pgcIndexViewModel.selectedFilters\n\n    val onLongClickSeason = {\n        if (filterReady) {\n            showFilter = true\n        }\n    }\n\n    val reloadData = {\n        scope.launch(Dispatchers.IO) {\n            pgcIndexViewModel.clearData()\n            pgcIndexViewModel.loadMore()\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        val intent = (context as Activity).intent\n        val pgcType = runCatching {\n            PgcType.entries[intent.getIntExtra(\"pgcType\", 0)]\n        }.onFailure {\n            logger.warn { \"get pgcType from intent failed: ${it.stackTraceToString()}\" }\n        }.getOrDefault(PgcType.Anime)\n        logger.fInfo { \"index pgcType: $pgcType\" }\n        pgcIndexViewModel.changePgcType(pgcType)\n    }\n\n    LaunchedEffect(filterReady, filterSignature) {\n        if (!filterReady) return@LaunchedEffect\n        reloadData()\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            Box(\n                modifier = Modifier.padding(start = 48.dp, top = 24.dp, bottom = 8.dp, end = 48.dp)\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.Bottom,\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Text(\n                        text = stringResource(id = R.string.title_activity_pgc_index) +\n                                \" - \" + pgcIndexViewModel.pgcType.getDisplayName(context),\n                        fontSize = titleFontSize.sp,\n                    )\n                    Text(\n                        text = stringResource(R.string.filter_dialog_open_tip),\n                        color = Color.White.copy(alpha = 0.6f)\n                    )\n                }\n            }\n        }\n    ) { innerPadding ->\n        ProvideListBringIntoViewSpec {\n            LazyVerticalGrid(\n                modifier = gridFocusRestorer.containerModifier(\n                    Modifier\n                        .padding(innerPadding)\n                        .blockDownFocusExitAtGridEnd(\n                            currentIndex = currentSeasonIndex,\n                            itemCount = pgcItems.size,\n                            columnCount = 6\n                        )\n                ),\n                columns = GridCells.Fixed(6),\n                contentPadding = PaddingValues(24.dp),\n                verticalArrangement = Arrangement.spacedBy(24.dp),\n                horizontalArrangement = Arrangement.spacedBy(24.dp)\n            ) {\n                itemsIndexed(\n                    items = pgcItems,\n                    key = { index, pgcItem -> \"$index-season-${pgcItem.seasonId}\" }\n                ) { index, pgcItem ->\n                    SeasonCard(\n                        modifier = gridFocusRestorer.firstItemModifier(index),\n                        data = SeasonCardData.fromPgcItem(pgcItem),\n                        onFocus = {\n                            currentSeasonIndex = index\n                            if (index + 30 > pgcItems.size) {\n                                println(\"load more by focus\")\n                                scope.launch(Dispatchers.IO) { pgcIndexViewModel.loadMore() }\n                            }\n                        },\n                        onClick = {\n                            SeasonInfoActivity.actionStart(\n                                context = context,\n                                seasonId = pgcItem.seasonId,\n                                proxyArea = ProxyArea.checkProxyArea(pgcItem.title)\n                            )\n                        },\n                        onLongClick = onLongClickSeason\n                    )\n                }\n                if (pgcItems.isEmpty() && noMore) {\n                    item(\n                        span = { GridItemSpan(6) }\n                    ) {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center\n                        ) {\n                            Column(\n                                horizontalAlignment = Alignment.CenterHorizontally,\n                                verticalArrangement = Arrangement.spacedBy(8.dp)\n                            ) {\n                                Text(text = stringResource(R.string.no_data))\n                                OutlinedButton(\n                                    enabled = filterReady,\n                                    onClick = onLongClickSeason\n                                ) {\n                                    Text(text = stringResource(R.string.filter_dialog_open_tip_click))\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    IndexFilter(\n        type = pgcIndexViewModel.pgcType,\n        show = showFilter && filterReady,\n        onDismissRequest = { showFilter = false },\n        sections = filterSections,\n        selectedFilters = selectedFilters,\n        onFilterChange = { pgcIndexViewModel.updateFilter(it) },\n        onResetFilters = { pgcIndexViewModel.resetFilters() }\n    )\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/pgc/TvContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.pgc\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.rounded.List\nimport androidx.compose.material.icons.rounded.QuestionMark\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.activities.pgc.PgcIndexActivity\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcTvViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun TvContent(\n    modifier: Modifier = Modifier,\n    lazyListState: LazyListState,\n    pgcViewModel: PgcTvViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n\n    val onOpenIndex: () -> Unit = {\n        PgcIndexActivity.actionStart(context = context, pgcType = PgcType.Tv)\n    }\n\n    PgcScaffold(\n        lazyListState = lazyListState,\n        pgcViewModel = pgcViewModel,\n        pgcType = PgcType.Tv,\n        featureButtons = {\n            TvFeatureButtons(\n                modifier = Modifier.padding(vertical = 24.dp),\n                onOpenIndex = onOpenIndex\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun TvFeatureButtons(\n    modifier: Modifier = Modifier,\n    onOpenIndex: () -> Unit\n) {\n    val buttons = listOf(\n        Triple(\n            stringResource(R.string.anime_home_button_index),\n            Icons.AutoMirrored.Rounded.List,\n            onOpenIndex\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        )\n    )\n    PgcFeatureButtons(\n        modifier = modifier,\n        buttons = buttons\n    )\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun TvFeatureButtonsPreview() {\n    BVTheme {\n        TvFeatureButtons(\n            modifier = Modifier,\n            onOpenIndex = {},\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/pgc/VarietyContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.pgc\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.rounded.List\nimport androidx.compose.material.icons.rounded.QuestionMark\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.activities.pgc.PgcIndexActivity\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.viewmodel.pgc.PgcVarietyViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun VarietyContent(\n    modifier: Modifier = Modifier,\n    lazyListState: LazyListState,\n    pgcViewModel: PgcVarietyViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n\n    val onOpenIndex: () -> Unit = {\n        PgcIndexActivity.actionStart(context = context, pgcType = PgcType.Variety)\n    }\n\n    PgcScaffold(\n        lazyListState = lazyListState,\n        pgcViewModel = pgcViewModel,\n        pgcType = PgcType.Variety,\n        featureButtons = {\n            VarietyFeatureButtons(\n                modifier = Modifier.padding(vertical = 24.dp),\n                onOpenIndex = onOpenIndex\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun VarietyFeatureButtons(\n    modifier: Modifier = Modifier,\n    onOpenIndex: () -> Unit\n) {\n    val buttons = listOf(\n        Triple(\n            stringResource(R.string.anime_home_button_index),\n            Icons.AutoMirrored.Rounded.List,\n            onOpenIndex\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        ),\n        Triple(\n            stringResource(R.string.pgc_home_button_unknown),\n            Icons.Rounded.QuestionMark,\n            showPlaceholderToast\n        )\n    )\n    PgcFeatureButtons(\n        modifier = modifier,\n        buttons = buttons\n    )\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun VarietyFeatureButtonsPreview() {\n    BVTheme {\n        VarietyFeatureButtons(\n            modifier = Modifier,\n            onOpenIndex = {},\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/pgc/anime/AnimeTimelineScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.pgc.anime\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.season.Timeline\nimport dev.aaa1115910.biliapi.entity.season.TimelineFilter\nimport dev.aaa1115910.biliapi.repositories.SeasonRepository\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.component.videocard.SeasonCard\nimport dev.aaa1115910.bv.entity.carddata.SeasonCardData\nimport dev.aaa1115910.bv.tv.activities.video.SeasonInfoActivity\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.addAllWithMainContext\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.util.resizedImageUrl\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.compose.getKoin\n\n@Composable\nfun AnimeTimelineScreen(\n    modifier: Modifier = Modifier,\n    seasonRepository: SeasonRepository = getKoin().get()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger { }\n    val listState = rememberLazyListState()\n    val defaultFocusRequester = remember { FocusRequester() }\n    val listFocusRestorer = rememberTvLazyListFocusRestorer(defaultFocusRequester)\n\n    var currentTimelineIndex by remember { mutableIntStateOf(0) }\n    var currentEpisodeIndex by remember { mutableIntStateOf(0) }\n    val showLargeTitle by remember {\n        derivedStateOf {\n            currentTimelineIndex == 0 && currentEpisodeIndex < 1\n        }\n    }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n\n    val timelines = remember { mutableStateListOf<Timeline>() }\n\n    LaunchedEffect(Unit) {\n        scope.launch(Dispatchers.IO) {\n            runCatching {\n                timelines.addAllWithMainContext {\n                    seasonRepository.getTimeline(\n                        filter = TimelineFilter.Anime,\n                        preferApiType = Prefs.apiType\n                    )\n                }\n                runCatching {\n                    delay(200)\n                    logger.info { \"scroll to item today\" }\n                    // web 接口可以获取到最大 7 天前的数据，而 app 接口只能从 6 天前开始获取\n                    val targetIndex = when (Prefs.apiType) {\n                        ApiType.Web -> 7\n                        ApiType.App -> 6\n                    }\n                    withContext(Dispatchers.Main) {\n                        listState.animateScrollToItem(targetIndex, 0)\n                        defaultFocusRequester.requestFocus(scope)\n                    }\n                }\n            }.onFailure {\n                logger.fInfo { \"Get timeline failed: ${it.stackTraceToString()}\" }\n                withContext(Dispatchers.Main) {\n                    \"获取放送时间表失败: ${it.message}\".toast(context)\n                }\n            }\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            Box(\n                modifier = Modifier.padding(start = 48.dp, top = 24.dp, bottom = 8.dp, end = 48.dp)\n            ) {\n                Text(\n                    text = stringResource(id = R.string.title_activity_anime_timeline),\n                    fontSize = titleFontSize.sp,\n                )\n            }\n        }\n    ) { innerPadding ->\n        LazyColumn(\n            state = listState,\n            modifier = listFocusRestorer.containerModifier(Modifier.padding(innerPadding)),\n            contentPadding = PaddingValues(bottom = 48.dp, start = 48.dp, end = 48.dp)\n        ) {\n            itemsIndexed(\n                items = timelines,\n                key = { index, timeline -> \"$index-timeline-${timeline.date.time}\" }\n            ) { index, timeline ->\n                val defaultModifier = if (timeline.isToday) {\n                    Modifier.focusRequester(defaultFocusRequester)\n                } else {\n                    Modifier\n                }\n                TimelinePerDay(\n                    modifier = defaultModifier,\n                    timeline = timeline,\n                    onFocusChange = { episodeIndex ->\n                        currentTimelineIndex = index\n                        currentEpisodeIndex = episodeIndex\n                    },\n                    onClick = { seasonId ->\n                        SeasonInfoActivity.actionStart(\n                            context = context,\n                            seasonId = seasonId\n                        )\n                    }\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun TimelinePerDay(\n    modifier: Modifier = Modifier,\n    timeline: Timeline,\n    onFocusChange: (index: Int) -> Unit = {},\n    onClick: (seasonId: Int) -> Unit = {},\n) {\n    var hasFocus by remember { mutableStateOf(false) }\n    val titleColor =\n        if (hasFocus) MaterialTheme.colorScheme.onSurface\n        else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (hasFocus) 30f else 20f,\n        label = \"title font size\"\n    )\n    val episodeChunkedList = timeline.episodes.chunked(5)\n\n    val getWeekString: (Int) -> String = { dayOfWeek ->\n        when (dayOfWeek) {\n            1 -> \"周一\"\n            2 -> \"周二\"\n            3 -> \"周三\"\n            4 -> \"周四\"\n            5 -> \"周五\"\n            6 -> \"周六\"\n            7 -> \"周日\"\n            else -> \"未知\"\n        }\n    }\n\n    Column(\n        modifier = modifier\n            .padding(top = 24.dp)\n            .onFocusChanged { hasFocus = it.hasFocus },\n        verticalArrangement = Arrangement.spacedBy(12.dp),\n    ) {\n        Text(\n            text = \"${timeline.dateString} ${getWeekString(timeline.dayOfWeek)}\",\n            fontSize = titleFontSize.sp,\n            color = titleColor\n        )\n\n        episodeChunkedList.forEachIndexed { index, episodes ->\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 6.dp, vertical = 12.dp),\n                horizontalArrangement = Arrangement.SpaceBetween\n            ) {\n                episodes.forEach { episode ->\n                    SeasonCard(\n                        modifier = Modifier\n                            .weight(1f)\n                            .padding(horizontal = 8.dp),\n                        data = SeasonCardData(\n                            title = episode.title,\n                            cover = episode.cover.resizedImageUrl(ImageSize.SeasonCoverThumbnail),\n                            seasonId = episode.seasonId,\n                            rating = null\n                        ),\n                        onFocus = { onFocusChange(index) },\n                        onClick = { onClick(episode.seasonId) }\n                    )\n                }\n                repeat(5 - episodes.size) {\n                    Spacer(modifier = Modifier.weight(1f))\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/ugc/UgcChildRegionButtons.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.ugc\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.ExperimentalTvMaterial3Api\nimport androidx.tv.material3.SuggestionChip\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.getDisplayName\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\n@OptIn(ExperimentalTvMaterial3Api::class)\n@Composable\nfun UgcChildRegionButtons(\n    modifier: Modifier = Modifier,\n    childUgcTypes: List<UgcTypeV2>\n) {\n//    val context = LocalContext.current\n//    val logger = KotlinLogging.logger { }\n//\n//    val onClickChildRegion: (UgcTypeV2) -> Unit = { ugcType ->\n//        logger.fInfo { \"onClickChildRegion: $ugcType\" }\n//        \"占位\".toast(context)\n//    }\n//\n//    UgcChildRegionButtonsContent(\n//        modifier = modifier\n//            .padding(vertical = 12.dp),\n//        childUgcTypes = childUgcTypes,\n//        onClickChildRegion = onClickChildRegion\n//    )\n}\n\n@OptIn(ExperimentalTvMaterial3Api::class)\n@Composable\nfun UgcChildRegionButtonsContent(\n    modifier: Modifier = Modifier,\n    childUgcTypes: List<UgcTypeV2>,\n    onClickChildRegion: (UgcTypeV2) -> Unit\n) {\n    val context = LocalContext.current\n    val focusRequester = remember { FocusRequester() }\n\n    LazyRow(\n        modifier = modifier.focusRestorer(focusRequester),\n        contentPadding = PaddingValues(horizontal = 24.dp),\n        horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),\n    ) {\n        itemsIndexed(\n            items = childUgcTypes,\n            key = { index, ugcType -> \"$index-ugc-${ugcType.name}\" }\n        ) { index, ugcType ->\n            SuggestionChip(\n                modifier = Modifier.ifElse(index == 0, Modifier.focusRequester(focusRequester)),\n                onClick = { onClickChildRegion(ugcType) }\n            ) {\n                Text(text = ugcType.getDisplayName(context))\n            }\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun UgcChildRegionButtonsPreview() {\n    BVTheme {\n        UgcChildRegionButtons(\n            modifier = Modifier.fillMaxWidth(),\n            childUgcTypes = UgcTypeV2.dougaList\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/ugc/UgcCommon.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.ugc\n\nimport android.content.Context\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.dimensionResource\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.biliapi.entity.CarouselData\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.entity.ugc.region.UgcFeedPage\nimport dev.aaa1115910.biliapi.repositories.UgcRepository\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.component.UgcCarousel\nimport dev.aaa1115910.bv.tv.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.tv.R\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.component.LoadingTip\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.compose.koinInject\n\n@Composable\nfun UgcRegionScaffold(\n    modifier: Modifier = Modifier,\n    lazyGridState: LazyGridState = rememberLazyGridState(),\n    ugcViewModel: UgcViewModel,\n    childRegionButtons: (@Composable () -> Unit)? = null\n) {\n    val context = LocalContext.current\n    val videoInfoRepository: VideoInfoRepository = koinInject()\n    val carouselFocusRestorer = rememberTvLazyListFocusRestorer()\n    val cardFocusRestorer = rememberTvLazyListFocusRestorer()\n    var currentFocusedIndex by remember { mutableIntStateOf(0) }\n    val shouldLoadMore by remember {\n        derivedStateOf { !ugcViewModel.ugcItems.isEmpty() && currentFocusedIndex + 12 > ugcViewModel.ugcItems.size }\n    }\n\n    val onLongClickVideo: (UgcItem) -> Unit = { ugcItem ->\n        UpInfoActivity.actionStart(\n            context,\n            mid = ugcItem.authorId,\n            name = ugcItem.author,\n            face = ugcItem.authorFace\n        )\n    }\n\n    LaunchedEffect(shouldLoadMore) {\n        if (shouldLoadMore) {\n            withContext(Dispatchers.IO) {\n                ugcViewModel.loadMore()\n            }\n        }\n    }\n\n    val padding = dimensionResource(R.dimen.grid_padding)\n    val spacedBy = dimensionResource(R.dimen.grid_spacedBy)\n    ProvideListBringIntoViewSpec {\n        LazyVerticalGrid(\n            modifier = if (ugcViewModel.showCarousel && ugcViewModel.carouselItems.isNotEmpty()) {\n                carouselFocusRestorer.containerModifier(\n                    modifier\n                        .fillMaxSize()\n                        .blockDownFocusExitAtGridEnd(\n                            currentIndex = currentFocusedIndex,\n                            itemCount = ugcViewModel.ugcItems.size,\n                            columnCount = 4\n                        )\n                )\n            } else {\n                cardFocusRestorer.containerModifier(\n                    modifier\n                        .fillMaxSize()\n                        .blockDownFocusExitAtGridEnd(\n                            currentIndex = currentFocusedIndex,\n                            itemCount = ugcViewModel.ugcItems.size,\n                            columnCount = 4\n                        )\n                )\n            },\n            columns = GridCells.Fixed(4),\n            state = lazyGridState,\n            contentPadding = PaddingValues(padding),\n            verticalArrangement = Arrangement.spacedBy(spacedBy),\n            horizontalArrangement = Arrangement.spacedBy(spacedBy)\n        ) {\n            // 轮播图组件\n            if (ugcViewModel.showCarousel && ugcViewModel.carouselItems.isNotEmpty()) {\n                item(span = { GridItemSpan(maxLineSpan) }) {\n                    UgcCarousel(\n                        modifier = carouselFocusRestorer.firstItemModifier(0, Modifier.fillMaxWidth()),\n                        data = ugcViewModel.carouselItems,\n                        onClick = { item -> \n                            videoInfoRepository.preloadedVideoList.clear()\n                            VideoInfoActivity.actionStart(\n                                context = context,\n                                aid = item.avid!!\n                            )\n                        }\n                    )\n                }\n            }\n\n            // 子区域按钮\n            // if (childRegionButtons != null) {\n            //     item(span = { GridItemSpan(maxLineSpan) }) {\n            //         childRegionButtons()\n            //     }\n            // }\n\n            itemsIndexed(\n                items = ugcViewModel.ugcItems,\n                key = { index, item -> \"$index-av-${item.aid}\" }\n            ) { index, item ->\n                SmallVideoCard(\n                    modifier = cardFocusRestorer.firstItemModifier(index),\n                    data = remember(item.aid) {\n                        VideoCardData(\n                            avid = item.aid,\n                            title = item.title,\n                            cover = item.cover,\n                            play = item.play,\n                            danmaku = item.danmaku,\n                            upName = item.author,\n                            time = item.duration * 1000L,\n                            pubTime = item.pubTime\n                        )\n                    },\n                    onClick = {\n                        videoInfoRepository.preloadedVideoList.clear()\n                        videoInfoRepository.preloadedVideoList.addAll(\n                            ugcViewModel.ugcItems.map { ugcItem ->\n                                VideoCardData(\n                                    avid = ugcItem.aid,\n                                    title = ugcItem.title,\n                                    cover = ugcItem.cover,\n                                    upName = ugcItem.author,\n                                    upId = ugcItem.authorId,\n                                    play = ugcItem.play,\n                                    danmaku = ugcItem.danmaku,\n                                    time = ugcItem.duration * 1000L,\n                                    pubTime = ugcItem.pubTime\n                                )\n                            }\n                        )\n                        VideoInfoActivity.actionStart(context, item.aid)\n                    },\n                    onLongClick = {onLongClickVideo(item) },\n                    onFocus = { currentFocusedIndex = index }\n                )\n            }\n\n            if (ugcViewModel.updating) {\n                item(span = { GridItemSpan(maxLineSpan) }) {    // 网格里占整行\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .height(80.dp),\n                        contentAlignment = Alignment.Center\n                    ) { LoadingTip() }\n                }\n            }\n        }\n    }\n}\n\ndata class UgcScaffoldState(\n    val context: Context,\n    val scope: CoroutineScope,\n    val lazyGridState: LazyGridState,\n    val ugcType: UgcTypeV2,\n    private val ugcRepository: UgcRepository\n) {\n    companion object {\n        val logger = KotlinLogging.logger { }\n\n        // 保存每个ugcType的数据状态\n        private val dataCache = mutableMapOf<UgcTypeV2, List<UgcItem>>()\n        private val pageCache = mutableMapOf<UgcTypeV2, UgcFeedPage>()\n\n        // 清除缓存，可以在内存不足或需要重新加载所有数据时调用\n        fun clearCache() {\n            dataCache.clear()\n            pageCache.clear()\n        }\n    }\n\n    // val carouselItems = mutableStateListOf<CarouselData.CarouselItem>()\n    val ugcItems = mutableStateListOf<UgcItem>()\n    var nextPage by mutableStateOf(pageCache[ugcType] ?: UgcFeedPage())\n    var hasMore by mutableStateOf(true)\n    var updating by mutableStateOf(false)\n    var dataInitialized by mutableStateOf(false)\n    // var showCarousel by mutableStateOf(true)\n    init {\n        // 如果有缓存数据，则恢复\n        dataCache[ugcType]?.let { cachedItems ->\n            if (cachedItems.isNotEmpty()) {\n                ugcItems.addAll(cachedItems)\n                dataInitialized = true\n                logger.fInfo { \"Restored ${cachedItems.size} items from cache for $ugcType\" }\n            }\n        }\n    }\n\n    suspend fun initUgcRegionData() {\n        loadUgcRegionData()\n    }\n    suspend fun loadUgcRegionData() {\n        if (!hasMore && updating) return\n        // 如果已经初始化了数据，就不再重新加载\n        if (dataInitialized) {\n            logger.fInfo { \"Data already initialized for $ugcType, skip loading\" }\n            return\n        }\n\n        updating = true\n        logger.fInfo { \"load ugc $ugcType region data\" }\n        runCatching {\n            val data = withContext(Dispatchers.IO) { ugcRepository.getRegionFeedRcmd(ugcType, nextPage) }\n            ugcItems.clear()\n            ugcItems.addAll(data.items)\n            nextPage = data.nextPage\n\n            updateCache()\n            dataInitialized = true\n            hasMore = true\n\n            // 初始化后加载更多内容\n            loadMore()\n        }.onFailure {\n            logger.fInfo { \"load $ugcType data failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"加载 $ugcType 数据失败: ${it.message}\".toast(context)\n            }\n            hasMore = false\n        }.also {\n            updating = false\n        }\n    }\n\n    // 将缓存更新逻辑提取为单独的函数\n    private fun updateCache() {\n        dataCache[ugcType] = ugcItems.toList()\n        pageCache[ugcType] = nextPage\n    }\n    fun reloadAll() {\n        logger.fInfo { \"reload all $ugcType data\" }\n        scope.launch {\n            withContext(Dispatchers.IO) {\n                dataCache.remove(ugcType)\n                pageCache.remove(ugcType)\n            }\n\n            nextPage = UgcFeedPage()\n            hasMore = true\n            ugcItems.clear()\n            dataInitialized = false\n\n            // 重新初始化数据\n            initUgcRegionData()\n        }\n    }\n    suspend fun loadMore() {\n        if (!hasMore && updating) return\n        updating = true\n        runCatching {\n            val data = withContext(Dispatchers.IO) { ugcRepository.getRegionFeedRcmd(ugcType, nextPage) }\n            ugcItems.addAll(data.items)\n            nextPage = data.nextPage\n            hasMore = data.items.isNotEmpty()\n\n            updateCache()\n        }.onFailure {\n            logger.fInfo { \"load more $ugcType data failed: ${it.stackTraceToString()}\" }\n            withContext(Dispatchers.Main) {\n                \"加载 $ugcType 更多推荐失败: ${it.message}\".toast(context)\n            }\n        }.also {\n            updating = false\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/ugc/UgcContentFactory.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.main.ugc\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.bv.tv.component.UgcTopNavItem\nimport dev.aaa1115910.bv.viewmodel.ugc.UgcViewModel\n\n/**\n * 通用UGC内容组件，用于替代所有重复的*Content.kt文件\n */\n@Composable\nfun GenericUgcContent(\n    modifier: Modifier = Modifier,\n    lazyGridState: LazyGridState = rememberLazyGridState(),\n    ugcViewModel: UgcViewModel,\n    childUgcTypes: List<UgcTypeV2>\n) {\n    UgcRegionScaffold(\n        modifier = modifier,\n        lazyGridState = lazyGridState,\n        ugcViewModel = ugcViewModel,\n        childRegionButtons = {\n            UgcChildRegionButtons(\n                modifier = Modifier.fillMaxWidth(),\n                childUgcTypes = childUgcTypes\n            )\n        }\n    )\n}\n\n/**\n * UGC内容工厂，根据TopNavItem创建对应的内容组件\n */\n@Composable\nfun CreateUgcContent(\n    navItem: UgcTopNavItem,\n    modifier: Modifier = Modifier,\n    lazyGridState: LazyGridState = rememberLazyGridState(),\n    ugcViewModel: UgcViewModel\n) {\n    val childUgcTypes = when (navItem) {\n        UgcTopNavItem.Douga -> UgcTypeV2.dougaList\n        UgcTopNavItem.Game -> UgcTypeV2.gameList\n        UgcTopNavItem.Kichiku -> UgcTypeV2.kichikuList\n        UgcTopNavItem.Music -> UgcTypeV2.musicList\n        UgcTopNavItem.Dance -> UgcTypeV2.danceList\n        UgcTopNavItem.Cinephile -> UgcTypeV2.cinephileList\n        UgcTopNavItem.Ent -> UgcTypeV2.entList\n        UgcTopNavItem.Knowledge -> UgcTypeV2.knowledgeList\n        UgcTopNavItem.Tech -> UgcTypeV2.techList\n        UgcTopNavItem.Information -> UgcTypeV2.informationList\n        UgcTopNavItem.Food -> UgcTypeV2.foodList\n        UgcTopNavItem.ShortPlay -> UgcTypeV2.shortplayList\n        UgcTopNavItem.Car -> UgcTypeV2.carList\n        UgcTopNavItem.Fashion -> UgcTypeV2.fashionList\n        UgcTopNavItem.Sports -> UgcTypeV2.sportsList\n        UgcTopNavItem.Animal -> UgcTypeV2.animalList\n        UgcTopNavItem.Vlog -> UgcTypeV2.vlogList\n        UgcTopNavItem.Painting -> UgcTypeV2.paintingList\n        UgcTopNavItem.Ai -> UgcTypeV2.aiList\n        UgcTopNavItem.Home -> UgcTypeV2.homeList\n        UgcTopNavItem.Outdoors -> UgcTypeV2.outdoorsList\n        UgcTopNavItem.Gym -> UgcTypeV2.gymList\n        UgcTopNavItem.Handmake -> UgcTypeV2.handmakeList\n        UgcTopNavItem.Travel -> UgcTypeV2.travelList\n        UgcTopNavItem.Rural -> UgcTypeV2.ruralList\n        UgcTopNavItem.Parenting -> UgcTypeV2.parentingList\n        UgcTopNavItem.Health -> UgcTypeV2.healthList\n        UgcTopNavItem.Emotion -> UgcTypeV2.emotionList\n        UgcTopNavItem.LifeJoy -> UgcTypeV2.lifeJoyList\n        UgcTopNavItem.LifeExperience -> UgcTypeV2.lifeExperienceList\n        UgcTopNavItem.Mysticism -> UgcTypeV2.mysticismList\n    }\n\n    GenericUgcContent(\n        modifier = modifier,\n        lazyGridState = lazyGridState,\n        ugcViewModel = ugcViewModel,\n        childUgcTypes = childUgcTypes\n    )\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/main/ugc/UgcStateManager.kt",
    "content": ""
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/search/SearchInputScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.search\n\nimport androidx.activity.compose.BackHandler\nimport android.content.res.Configuration\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.focusGroup\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport dev.aaa1115910.bv.tv.screens.main.drawerItemFocusRequesters\nimport dev.aaa1115910.bv.tv.screens.main.DrawerItem\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.DeleteSweep\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.OutlinedTextFieldDefaults\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusDirection\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.ButtonDefaults\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.IconButton\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.search.Hotword\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.db.SearchHistoryDB\nimport dev.aaa1115910.bv.tv.activities.search.SearchResultActivity\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.search.SearchKeyword\nimport dev.aaa1115910.bv.tv.component.search.SoftKeyboard\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.viewmodel.search.SearchInputViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun SearchInputScreen(\n    modifier: Modifier = Modifier,\n    defaultFocusRequester: FocusRequester,\n    searchInputViewModel: SearchInputViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n\n    val searchKeyword = searchInputViewModel.keyword\n    val hotwords = searchInputViewModel.hotwords\n    val searchHistories = searchInputViewModel.searchHistories\n    val suggests = searchInputViewModel.suggests\n\n    var enableProxy by remember { mutableStateOf(false) }\n\n    var focusOnContent by remember { mutableStateOf(false) }\n\n    val onSearch: (String) -> Unit = { keyword ->\n        SearchResultActivity.actionStart(context, keyword, enableProxy)\n        searchInputViewModel.keyword = keyword\n        searchInputViewModel.addSearchHistory(keyword)\n    }\n\n    LaunchedEffect(searchKeyword) {\n        searchInputViewModel.updateSuggests()\n    }\n\n    BackHandler(enabled = focusOnContent) {\n        drawerItemFocusRequesters[DrawerItem.Search]?.requestFocus()\n    }\n\n    SearchInputScreenContent(\n        modifier = modifier\n            .onFocusChanged { focusOnContent = it.hasFocus },\n        defaultFocusRequester = defaultFocusRequester,\n        searchKeyword = searchKeyword,\n        onSearchKeywordChange = { searchInputViewModel.keyword = it },\n        onSearch = onSearch,\n        showProxyOptions = Prefs.enableProxy,\n        enableProxy = enableProxy,\n        onEnableProxyChange = { enableProxy = it },\n        hotwords = hotwords,\n        suggests = suggests,\n        histories = searchHistories,\n        onDeleteHistory = { searchInputViewModel.deleteSearchHistory(it) },\n        onDeleteAllHistories = { searchInputViewModel.deleteAllSearchHistories() }\n    )\n}\n\n@Composable\nprivate fun SearchInputScreenContent(\n    modifier: Modifier = Modifier,\n    defaultFocusRequester: FocusRequester,\n    searchKeyword: String,\n    onSearchKeywordChange: (String) -> Unit,\n    onSearch: (String) -> Unit,\n    showProxyOptions: Boolean,\n    enableProxy: Boolean,\n    onEnableProxyChange: (Boolean) -> Unit,\n    hotwords: List<Hotword>,\n    suggests: List<String>,\n    histories: List<SearchHistoryDB>,\n    onDeleteHistory: (SearchHistoryDB) -> Unit,\n    onDeleteAllHistories: () -> Unit\n) {\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            Box(\n                modifier = Modifier.padding(start = 48.dp, top = 24.dp, bottom = 8.dp, end = 48.dp)\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.Bottom,\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Text(\n                        text = stringResource(R.string.search_input_title),\n                        fontSize = 48.sp\n                    )\n                }\n            }\n        }\n    ) { innerPadding ->\n        Row(\n            modifier = Modifier\n                .padding(innerPadding)\n                .padding(vertical = 8.dp)\n                .padding(start = 24.dp)\n                .horizontalScroll(rememberScrollState()),\n            horizontalArrangement = Arrangement.spacedBy(20.dp)\n        ) {\n            SearchInput(\n                firstButtonFocusRequester = defaultFocusRequester,\n                searchKeyword = searchKeyword,\n                onSearchKeywordChange = onSearchKeywordChange,\n                onSearch = { onSearch(searchKeyword) },\n                showProxyOptions = showProxyOptions,\n                enableProxy = enableProxy,\n                onEnableProxyChange = onEnableProxyChange\n            )\n\n            if (searchKeyword.isEmpty()) {\n                SearchHotwords(\n                    hotwords = hotwords,\n                    onSearch = onSearch\n                )\n            } else {\n                SearchSuggestion(\n                    suggests = suggests,\n                    onSearch = onSearch\n                )\n            }\n\n            SearchHistory(\n                modifier = Modifier\n                    .padding(end = 10.dp),\n                histories = histories,\n                onSearch = onSearch,\n                onDelete = onDeleteHistory,\n                onDeleteAll = onDeleteAllHistories\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun SearchInput(\n    modifier: Modifier = Modifier,\n    firstButtonFocusRequester: FocusRequester,\n    searchKeyword: String,\n    onSearchKeywordChange: (String) -> Unit,\n    onSearch: (String) -> Unit,\n    showProxyOptions: Boolean,\n    enableProxy: Boolean,\n    onEnableProxyChange: (Boolean) -> Unit\n) {\n    var textFieldValue by remember {\n        mutableStateOf(\n            TextFieldValue(\n                text = searchKeyword,\n                selection = TextRange(searchKeyword.length)\n            )\n        )\n    }\n\n    LaunchedEffect(searchKeyword) {\n        if (searchKeyword != textFieldValue.text) {\n            textFieldValue = textFieldValue.copy(\n                text = searchKeyword,\n                selection = TextRange(searchKeyword.length)\n            )\n        }\n    }\n\n    Box(\n        modifier = modifier\n            .width(280.dp)\n            .fillMaxHeight()\n            .focusGroup(),\n        contentAlignment = Alignment.TopCenter\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(16.dp)\n        ) {\n            OutlinedTextField(\n                modifier = Modifier\n                    .width(258.dp)\n                    .onFocusChanged {\n                        if (it.isFocused && textFieldValue.selection.end != textFieldValue.text.length) {\n                            textFieldValue = textFieldValue.copy(\n                                selection = TextRange(textFieldValue.text.length)\n                            )\n                        }\n                    },\n                value = textFieldValue,\n                onValueChange = {\n                    textFieldValue = it\n                    onSearchKeywordChange(it.text)\n                },\n                maxLines = 1,\n                shape = MaterialTheme.shapes.medium,\n                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n                keyboardActions = KeyboardActions(onSearch = { onSearch(searchKeyword) }),\n                colors = OutlinedTextFieldDefaults.colors(\n                    focusedBorderColor = MaterialTheme.colorScheme.inverseSurface,\n                    cursorColor = MaterialTheme.colorScheme.inverseSurface\n                )\n            )\n            SoftKeyboard(\n                firstButtonFocusRequester = firstButtonFocusRequester,\n                showSearchWithProxy = showProxyOptions,\n                enableSearchWithProxy = enableProxy,\n                onClick = { onSearchKeywordChange(searchKeyword + it) },\n                onClear = { onSearchKeywordChange(\"\") },\n                onDelete = {\n                    if (searchKeyword.isNotEmpty()) {\n                        onSearchKeywordChange(searchKeyword.dropLast(1))\n                    }\n                },\n                onSearch = { onSearch(searchKeyword) },\n                onEnableSearchWithProxyChange = onEnableProxyChange\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun SearchHotwords(\n    modifier: Modifier = Modifier,\n    hotwords: List<Hotword>,\n    onSearch: (String) -> Unit\n) {\n    Column(\n        modifier = modifier\n            .width(250.dp)\n            .fillMaxHeight()\n            .focusGroup(),\n    ) {\n        Text(\n            modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),\n            text = stringResource(R.string.search_input_hotword),\n            style = MaterialTheme.typography.titleLarge\n        )\n        LazyColumn(\n            modifier = Modifier,\n            contentPadding = PaddingValues(vertical = 4.dp)\n        ) {\n            itemsIndexed(\n                items = hotwords,\n                key = { index, hotword -> \"$index-hotword-${hotword.showName}\" }\n            ) { _, hotword ->\n                SearchKeyword(\n                    modifier = Modifier,\n                    keyword = hotword.showName,\n                    leadingIcon = hotword.icon ?: \"\",\n                    onClick = { onSearch(hotword.showName) }\n                )\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun SearchSuggestion(\n    modifier: Modifier = Modifier,\n    suggests: List<String>,\n    onSearch: (String) -> Unit\n) {\n    Column(\n        modifier = modifier\n            .width(250.dp)\n            .fillMaxHeight()\n            .focusGroup(),\n    ) {\n        Text(\n            modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),\n            text = stringResource(R.string.search_input_suggest),\n            style = MaterialTheme.typography.titleLarge\n        )\n        LazyColumn(\n            modifier = Modifier,\n            contentPadding = PaddingValues(vertical = 4.dp)\n        ) {\n            itemsIndexed(\n                items = suggests,\n                key = { index, suggest -> \"$index-suggest-$suggest\" }\n            ) { _, suggest ->\n                SearchKeyword(\n                    modifier = Modifier,\n                    keyword = suggest,\n                    leadingIcon = \"\",\n                    onClick = { onSearch(suggest) }\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SearchHistory(\n    modifier: Modifier = Modifier,\n    histories: List<SearchHistoryDB>,\n    onSearch: (String) -> Unit,\n    onDelete: (SearchHistoryDB) -> Unit,\n    onDeleteAll: () -> Unit\n) {\n    val focusManager = LocalFocusManager.current\n\n    var deleteMode by remember { mutableStateOf(false) }\n    var showDeleteAllConfirmDialog by remember { mutableStateOf(false) }\n\n    Column(\n        modifier = modifier\n            .width(250.dp)\n            .fillMaxHeight()\n            .focusGroup(),\n    ) {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.SpaceBetween\n        ) {\n            Text(\n                modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),\n                text = stringResource(R.string.search_input_history),\n                style = MaterialTheme.typography.titleLarge\n            )\n            Row {\n                if (deleteMode) {\n                    IconButton(\n                        onClick = { showDeleteAllConfirmDialog = true },\n                        colors = ButtonDefaults.colors(\n                            containerColor = MaterialTheme.colorScheme.surface,\n                        )\n                    ) {\n                        Icon(imageVector = Icons.Default.DeleteSweep, contentDescription = null)\n                    }\n                }\n                IconButton(\n                    onClick = { deleteMode = !deleteMode },\n                    colors = ButtonDefaults.colors(\n                        containerColor = MaterialTheme.colorScheme.surface,\n                    )\n                ) {\n                    if (deleteMode) {\n                        Icon(imageVector = Icons.Default.Close, contentDescription = null)\n                    } else {\n                        Icon(imageVector = Icons.Default.Delete, contentDescription = null)\n                    }\n                }\n            }\n        }\n\n        LazyColumn(\n            modifier = Modifier,\n            contentPadding = PaddingValues(vertical = 4.dp)\n        ) {\n            itemsIndexed(\n                items = histories,\n                key = { index, searchHistory -> \"$index-history-${searchHistory.id ?: searchHistory.keyword}\" }\n            ) { index, searchHistory ->\n                SearchKeyword(\n                    modifier = Modifier,\n                    keyword = searchHistory.keyword,\n                    leadingIcon = \"\",\n                    onClick = {\n                        if (deleteMode) {\n                            if (index == histories.lastIndex) {\n                                focusManager.moveFocus(FocusDirection.Up)\n                            }\n                            onDelete(searchHistory)\n                        } else {\n                            onSearch(searchHistory.keyword)\n                        }\n                    },\n                    trailingIcon = (@Composable {\n                        Icon(\n                            modifier = Modifier.size(16.dp),\n                            imageVector = Icons.Default.Delete,\n                            contentDescription = null\n                        )\n                    }).takeIf { deleteMode }\n                )\n            }\n        }\n    }\n\n    if (showDeleteAllConfirmDialog) {\n        TvAlertDialog(\n            onDismissRequest = { showDeleteAllConfirmDialog = false },\n            title = {\n                Text(text = stringResource(R.string.search_input_history_delete_all_confirm_dialog_title))\n            },\n            text = {\n                Text(text = stringResource(R.string.search_input_history_delete_all_confirm_dialog_text))\n            },\n            confirmButton = {\n                Button(onClick = {\n                    onDeleteAll()\n                    showDeleteAllConfirmDialog = false\n                }) {\n                    Text(text = stringResource(R.string.search_input_history_delete_all_confirm_dialog_confirm_button))\n                }\n            },\n            dismissButton = {\n                Button(onClick = {\n                    showDeleteAllConfirmDialog = false\n                }) {\n                    Text(text = stringResource(R.string.search_input_history_delete_all_confirm_dialog_cancel_button))\n                }\n            }\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun SearchInputScreenContentPreview() {\n    BVTheme {\n        Row {\n            Spacer(\n                modifier = Modifier\n                    .width(80.dp)\n                    .fillMaxHeight()\n                    .background(MaterialTheme.colorScheme.surfaceVariant)\n            )\n            SearchInputScreenContent(\n                modifier = Modifier,\n                defaultFocusRequester = FocusRequester.Default,\n                searchKeyword = \"测试\",\n                onSearchKeywordChange = {},\n                onSearch = {},\n                showProxyOptions = true,\n                enableProxy = false,\n                onEnableProxyChange = {},\n                hotwords = listOf(\n                    Hotword(\"热搜1\", \"热搜1\", null),\n                    Hotword(\"热搜2\", \"热搜2\", null)\n                ),\n                suggests = listOf(\"建议1\", \"建议2\"),\n                histories = listOf(\n                    SearchHistoryDB(keyword = \"历史1\"),\n                    SearchHistoryDB(keyword = \"历史2\")\n                ),\n                onDeleteHistory = {},\n                onDeleteAllHistories = {}\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/search/SearchResultFilter.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.search\n\nimport android.content.Context\nimport android.view.KeyEvent\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FilterChipDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport kotlinx.coroutines.delay\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.repositories.SearchFilterDuration\nimport dev.aaa1115910.biliapi.repositories.SearchFilterOrderType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.util.Partition\nimport dev.aaa1115910.bv.util.PartitionUtil\n\n@Composable\nfun SearchResultVideoFilter(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideFilter: () -> Unit,\n    selectedOrder: SearchFilterOrderType,\n    selectedDuration: SearchFilterDuration,\n    selectedPartition: Partition?,\n    selectedChildPartition: Partition?,\n    onSelectedOrderChange: (SearchFilterOrderType) -> Unit,\n    onSelectedDurationChange: (SearchFilterDuration) -> Unit,\n    onSelectedPartitionChange: (Partition?) -> Unit,\n    onSelectedChildPartitionChange: (Partition?) -> Unit,\n) {\n    val context = LocalContext.current\n    val partitions = remember { PartitionUtil.partitions }\n    val defaultFocusRequester = remember { FocusRequester() }\n    val durationFocusRequester = remember { FocusRequester() }\n    val partitionFocusRequester = remember { FocusRequester() }\n    val partitionChildFocusRequester = remember { FocusRequester() }\n\n    // 用于防止对话框刚打开时误触发点击事件\n    var isDialogJustOpened by remember { mutableStateOf(false) }\n\n    val filterRowSpace = 8.dp\n\n    LaunchedEffect(show) {\n        if (show) {\n            isDialogJustOpened = true\n            // 延迟请求焦点，避免打开对话框的按键事件被新获得焦点的组件消费\n            delay(100)\n            defaultFocusRequester.requestFocus()\n            // 等待一段时间后才允许点击事件\n            delay(200)\n            isDialogJustOpened = false\n        }\n    }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier\n                .fillMaxWidth(0.8f),\n            onDismissRequest = onHideFilter,\n            title = { Text(text = stringResource(R.string.filter_dialog_title)) },\n            text = {\n                Column {\n                    LazyRow(\n                        modifier = Modifier.onPreviewKeyEvent {\n                            if (it.key == Key.DirectionDown) {\n                                if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {\n                                    durationFocusRequester.requestFocus()\n                                    return@onPreviewKeyEvent true\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                            false\n                        },\n                        horizontalArrangement = Arrangement.spacedBy(filterRowSpace)\n                    ) {\n                        itemsIndexed(\n                            items = SearchFilterOrderType.webFilters,\n                            key = { index, orderType -> \"$index-order-${orderType.name}\" }\n                        ) { _, orderType ->\n                            FilterDialogFilterChip(\n                                focusRequester = defaultFocusRequester,\n                                selected = orderType == selectedOrder,\n                                onClick = { onSelectedOrderChange(orderType) },\n                                label = { Text(text = orderType.getDisplayName(context)) },\n                                enabled = !isDialogJustOpened\n                            )\n                        }\n                    }\n                    LazyRow(\n                        modifier = Modifier.onPreviewKeyEvent {\n                            if (it.key == Key.DirectionDown) {\n                                if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {\n                                    partitionFocusRequester.requestFocus()\n                                    return@onPreviewKeyEvent true\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                            if (it.key == Key.DirectionUp) {\n                                if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {\n                                    defaultFocusRequester.requestFocus()\n                                    return@onPreviewKeyEvent true\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                            false\n                        },\n                        horizontalArrangement = Arrangement.spacedBy(filterRowSpace)\n                    ) {\n                        itemsIndexed(\n                            items = SearchFilterDuration.entries,\n                            key = { index, duration -> \"$index-duration-${duration.name}\" }\n                        ) { _, duration ->\n                            FilterDialogFilterChip(\n                                focusRequester = durationFocusRequester,\n                                selected = duration == selectedDuration,\n                                onClick = { onSelectedDurationChange(duration) },\n                                label = { Text(text = duration.getDisplayName(context)) },\n                                enabled = !isDialogJustOpened\n                            )\n                        }\n                    }\n                    LazyRow(\n                        modifier = Modifier.onPreviewKeyEvent {\n                            if (it.key == Key.DirectionDown) {\n                                if (selectedChildPartition == null) return@onPreviewKeyEvent false\n                                if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {\n                                    partitionChildFocusRequester.requestFocus()\n                                    return@onPreviewKeyEvent true\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                            if (it.key == Key.DirectionUp) {\n                                if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {\n                                    durationFocusRequester.requestFocus()\n                                    return@onPreviewKeyEvent true\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                            false\n                        },\n                        horizontalArrangement = Arrangement.spacedBy(filterRowSpace)\n                    ) {\n                        item {\n                            FilterDialogFilterChip(\n                                focusRequester = partitionFocusRequester,\n                                selected = null == selectedPartition,\n                                onClick = {\n                                    onSelectedPartitionChange(null)\n                                    onSelectedChildPartitionChange(null)\n                                },\n                                label = { Text(text = \"全部分区\") },\n                                enabled = !isDialogJustOpened\n                            )\n                        }\n                        itemsIndexed(\n                            items = partitions,\n                            key = { index, partition -> \"$index-partition-${partition.tid}\" }\n                        ) { _, partition ->\n                            FilterDialogFilterChip(\n                                focusRequester = partitionFocusRequester,\n                                selected = partition == selectedPartition,\n                                onClick = {\n                                    onSelectedPartitionChange(partition)\n                                    onSelectedChildPartitionChange(null)\n                                },\n                                label = { Text(text = partition.strRes) },\n                                enabled = !isDialogJustOpened\n                            )\n                        }\n                    }\n                    AnimatedVisibility(visible = selectedPartition != null) {\n                        LazyRow(\n                            modifier = Modifier.onPreviewKeyEvent {\n                                if (it.key == Key.DirectionUp) {\n                                    if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {\n                                        partitionFocusRequester.requestFocus()\n                                        return@onPreviewKeyEvent true\n                                    }\n                                    return@onPreviewKeyEvent true\n                                }\n                                false\n                            },\n                            horizontalArrangement = Arrangement.spacedBy(filterRowSpace)\n                        ) {\n                            itemsIndexed(\n                                items = selectedPartition?.children ?: emptyList(),\n                                key = { index, partition -> \"$index-child-${partition.tid}\" }\n                            ) { _, partition ->\n                                FilterDialogFilterChip(\n                                    focusRequester = partitionChildFocusRequester,\n                                    selected = partition == selectedChildPartition,\n                                    onClick = {\n                                        onSelectedChildPartitionChange(\n                                            if (partition != selectedChildPartition) partition\n                                            else null\n                                        )\n                                    },\n                                    label = { Text(text = partition.strRes) },\n                                    enabled = !isDialogJustOpened\n                                )\n                            }\n                        }\n                    }\n                }\n            },\n            confirmButton = {},\n            properties = DialogProperties(usePlatformDefaultWidth = false)\n        )\n    }\n\n    BackHandler(\n        enabled = show,\n        onBack = onHideFilter\n    )\n}\n\n@Composable\nprivate fun FilterDialogFilterChip(\n    modifier: Modifier = Modifier,\n    focusRequester: FocusRequester,\n    selected: Boolean,\n    onClick: () -> Unit,\n    label: @Composable () -> Unit,\n    enabled: Boolean = true\n) {\n    var hasFocus by remember { mutableStateOf(false) }\n    val focusRequesterModifier = if (selected)\n        modifier.focusRequester(focusRequester)\n    else modifier\n\n    FilterChip(\n        modifier = focusRequesterModifier.onFocusChanged { hasFocus = it.hasFocus },\n        selected = selected,\n        onClick = { if (enabled) onClick() },\n        label = label,\n        border = if (hasFocus) FilterChipDefaults.filterChipBorder(\n            enabled = true,\n            selected = selected,\n            borderColor = MaterialTheme.colorScheme.border,\n            borderWidth = 2.dp,\n            selectedBorderColor = MaterialTheme.colorScheme.border,\n            selectedBorderWidth = 2.dp\n        )\n        else FilterChipDefaults.filterChipBorder(\n            enabled = true,\n            selected = selected\n        )\n    )\n}\n\nfun SearchFilterOrderType.getDisplayName(context: Context) = when (this) {\n    SearchFilterOrderType.ComprehensiveSort -> context.getString(R.string.search_result_filter_order_type_comprehensive_sort)\n    SearchFilterOrderType.MostClicks -> context.getString(R.string.search_result_filter_order_type_most_clicks)\n    SearchFilterOrderType.LatestPublish -> context.getString(R.string.search_result_filter_order_type_latest_publish)\n    SearchFilterOrderType.MostDanmaku -> context.getString(R.string.search_result_filter_order_type_most_danmaku)\n    SearchFilterOrderType.MostFavorites -> context.getString(R.string.search_result_filter_order_type_most_favorites)\n    SearchFilterOrderType.MostComment -> \"最多评论\"\n    SearchFilterOrderType.MostLikes -> \"最多点赞\"\n}\n\nfun SearchFilterDuration.getDisplayName(context: Context) = when (this) {\n    SearchFilterDuration.All -> context.getString(R.string.search_result_filter_duration_all)\n    SearchFilterDuration.LessThan10Minutes -> context.getString(R.string.search_result_filter_duration_less_than_10)\n    SearchFilterDuration.Between10And30Minutes -> context.getString(R.string.search_result_filter_duration_10_to_30)\n    SearchFilterDuration.Between30And60Minutes -> context.getString(R.string.search_result_filter_duration_30_to_60)\n    SearchFilterDuration.MoreThan60Minutes -> context.getString(R.string.search_result_filter_duration_more_than_60)\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/search/SearchResultScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.search\n\nimport android.app.Activity\nimport android.content.Context\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.live.LiveRoomItem\nimport dev.aaa1115910.biliapi.entity.ugc.toSmartDate\nimport dev.aaa1115910.biliapi.repositories.SearchType\nimport dev.aaa1115910.biliapi.repositories.SearchTypeResult\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.NavSwitchMode\nimport dev.aaa1115910.bv.tv.component.videocard.SeasonCard\nimport dev.aaa1115910.bv.tv.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.entity.carddata.SeasonCardData\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.activities.video.SeasonInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoPlayerV3Activity\nimport dev.aaa1115910.bv.tv.component.TopNav\nimport dev.aaa1115910.bv.tv.component.TopNavItem\nimport dev.aaa1115910.bv.tv.component.live.LiveRoomCard\nimport dev.aaa1115910.bv.tv.screens.user.UpCard\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.focusedScale\nimport dev.aaa1115910.bv.util.removeHtmlTags\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.viewmodel.search.SearchResultViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun SearchResultScreen(\n    modifier: Modifier = Modifier,\n    searchResultViewModel: SearchResultViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger { }\n    val navSwitchMode by Prefs.navSwitchModeFlow.collectAsState(Prefs.navSwitchMode)\n    val tabRowFocusRequester = remember { FocusRequester() }\n    val listFocusRestorer = rememberTvLazyListFocusRestorer()\n    val searchTopNavItems = remember { SearchType.entries.map(::SearchTopNavItem) }\n\n    var rowSize by remember { mutableIntStateOf(4) }\n    var currentIndex by remember { mutableIntStateOf(0) }\n    val showLargeTitle by remember { derivedStateOf { currentIndex < rowSize } }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"Title font size\"\n    )\n\n    var searchKeyword by remember { mutableStateOf(\"\") }\n\n    val searchResult = when (searchResultViewModel.searchType) {\n        SearchType.Video -> searchResultViewModel.videoSearchResult\n        SearchType.MediaBangumi -> searchResultViewModel.mediaBangumiSearchResult\n        SearchType.MediaFt -> searchResultViewModel.mediaFtSearchResult\n        SearchType.BiliUser -> searchResultViewModel.biliUserSearchResult\n        SearchType.LiveRoom -> searchResultViewModel.liveRoomSearchResult\n    }\n\n    var showFilter by remember { mutableStateOf(false) }\n\n    val selectedOrder = searchResultViewModel.selectedOrder\n    val selectedDuration = searchResultViewModel.selectedDuration\n    val selectedPartition = searchResultViewModel.selectedPartition\n    val selectedChildPartition = searchResultViewModel.selectedChildPartition\n\n    val onClickResult: (SearchTypeResult.SearchTypeResultItem) -> Unit = { resultItem ->\n        when (resultItem) {\n            is SearchTypeResult.Video -> {\n                VideoInfoActivity.actionStart(\n                    context = context,\n                    aid = resultItem.aid,\n                    fromSeason = false\n                )\n            }\n\n            is SearchTypeResult.Pgc -> {\n                SeasonInfoActivity.actionStart(\n                    context = context,\n                    seasonId = resultItem.seasonId,\n                    proxyArea = ProxyArea.checkProxyArea(resultItem.title)\n                )\n            }\n\n            is SearchTypeResult.User -> {\n                UpInfoActivity.actionStart(\n                    context = context,\n                    mid = resultItem.mid,\n                    name = resultItem.name,\n                    face = resultItem.avatar\n                )\n            }\n\n            is SearchTypeResult.LiveRoom -> {\n                VideoPlayerV3Activity.actionStartLive(\n                    context = context,\n                    roomId = resultItem.roomId.toInt(),\n                    title = resultItem.title,\n                    upId = resultItem.uid,\n                    upName = resultItem.uname,\n                    upFace = resultItem.face,\n                    watchedNum = resultItem.online / 10\n                )\n            }\n\n            else -> {}\n        }\n    }\n\n    val backToTabRow: () -> Unit = {\n        tabRowFocusRequester.requestFocus(scope)\n    }\n\n    val onLongClickSearchResultItem = {\n        if (searchResultViewModel.searchType == SearchType.Video) {\n            if (Prefs.apiType == ApiType.Web) showFilter = true\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        val intent = (context as Activity).intent\n        if (intent.hasExtra(\"keyword\")) {\n            searchKeyword = intent.getStringExtra(\"keyword\") ?: \"\"\n            val enableProxy = intent.getBooleanExtra(\"enableProxy\", false)\n            if (searchKeyword == \"\") context.finish()\n            searchResultViewModel.enableProxySearchResult = enableProxy\n            searchResultViewModel.keyword = searchKeyword\n        } else {\n            context.finish()\n        }\n    }\n\n    LaunchedEffect(searchResultViewModel.searchType) {\n        rowSize = when (searchResultViewModel.searchType) {\n            SearchType.Video -> 4\n            SearchType.MediaBangumi, SearchType.MediaFt -> 6\n            SearchType.BiliUser -> 3\n            SearchType.LiveRoom -> 4\n        }\n    }\n\n    LaunchedEffect(\n        selectedOrder, selectedDuration, selectedPartition, selectedChildPartition\n    ) {\n        logger.fInfo { \"Start update search result because filter updated\" }\n        searchResultViewModel.update()\n    }\n\n    LaunchedEffect(currentIndex) {\n        if (currentIndex + 12 > searchResult.count) {\n            searchResultViewModel.loadMore(searchResult.type)\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            Box(\n                modifier = Modifier.padding(start = 48.dp, top = 24.dp, bottom = 8.dp, end = 48.dp)\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.Bottom,\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Text(\n                        modifier = Modifier.fillMaxWidth(0.7f),\n                        text = searchKeyword,\n                        fontSize = titleFontSize.sp,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis\n                    )\n                    Column(\n                        horizontalAlignment = Alignment.End,\n                    ) {\n                        if (searchResultViewModel.searchType == SearchType.Video) {\n                            Text(\n                                text = stringResource(R.string.filter_dialog_open_tip),\n                                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                            )\n                        }\n                        Text(\n                            text = stringResource(\n                                R.string.load_data_count,\n                                searchResult.count\n                            ),\n                            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                        )\n                    }\n                }\n            }\n        }\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize()\n        ) {\n            TopNav(\n                paddingTop = 0.dp,\n                items = searchTopNavItems,\n                initialSelectedItem = searchTopNavItems.firstOrNull {\n                    it.searchType == searchResultViewModel.searchType\n                },\n                navSwitchMode = navSwitchMode,\n                tabFocusRequester = tabRowFocusRequester,\n                onSelectedChanged = { selectedItem ->\n                    val selectedSearchType = (selectedItem as SearchTopNavItem).searchType\n                    if (searchResultViewModel.searchType != selectedSearchType) {\n                        scope.launch {\n                            searchResultViewModel.searchType = selectedSearchType\n                            searchResultViewModel.init(selectedSearchType)\n                        }\n                    }\n                }\n            )\n            ProvideListBringIntoViewSpec(padding = 26.dp) {\n                LazyVerticalGrid(\n                    modifier = listFocusRestorer.containerModifier(\n                        Modifier\n                            .blockDownFocusExitAtGridEnd(\n                                currentIndex = currentIndex,\n                                itemCount = searchResult.count,\n                                columnCount = rowSize\n                            )\n                            .onPreviewKeyEvent {\n                                when (it.key) {\n                                    Key.Back -> {\n                                        if (it.type == KeyEventType.KeyUp) backToTabRow()\n                                        return@onPreviewKeyEvent true\n                                    }\n                                }\n                                false\n                            }\n                    ),\n                    columns = GridCells.Fixed(rowSize),\n                    contentPadding = PaddingValues(24.dp),\n                    verticalArrangement = Arrangement.spacedBy(24.dp),\n                    horizontalArrangement = Arrangement.spacedBy(24.dp)\n                ) {\n                    itemsIndexed(\n                        items = when (searchResult.type) {\n                            SearchType.Video -> searchResult.videos\n                            SearchType.MediaBangumi -> searchResult.mediaBangumis\n                            SearchType.MediaFt -> searchResult.mediaFts\n                            SearchType.BiliUser -> searchResult.biliUsers\n                            SearchType.LiveRoom -> searchResult.liveRooms\n                        },\n                        key = { index, item -> \"$index-${searchResultItemKey(item)}\" }\n                    ) { index, searchResultItem ->\n                        SearchResultListItem(\n                            modifier = listFocusRestorer.firstItemModifier(index),\n                            searchResult = searchResultItem,\n                            onClick = { onClickResult(searchResultItem) },\n                            onLongClick = onLongClickSearchResultItem,\n                            onFocus = { currentIndex = index }\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    SearchResultVideoFilter(\n        show = showFilter,\n        onHideFilter = { showFilter = false },\n        selectedOrder = selectedOrder,\n        selectedDuration = selectedDuration,\n        selectedPartition = selectedPartition,\n        selectedChildPartition = selectedChildPartition,\n        onSelectedOrderChange = { searchResultViewModel.selectedOrder = it },\n        onSelectedDurationChange = { searchResultViewModel.selectedDuration = it },\n        onSelectedPartitionChange = { searchResultViewModel.selectedPartition = it },\n        onSelectedChildPartitionChange = { searchResultViewModel.selectedChildPartition = it }\n    )\n}\n\nprivate data class SearchTopNavItem(\n    val searchType: SearchType\n) : TopNavItem {\n    override fun getDisplayName(context: Context): String {\n        return searchType.getDisplayName(context)\n    }\n}\n\nprivate fun searchResultItemKey(item: SearchTypeResult.SearchTypeResultItem): Any {\n    return when (item) {\n        is SearchTypeResult.Video -> \"video-${item.aid}\"\n        is SearchTypeResult.Pgc -> \"pgc-${item.seasonId}\"\n        is SearchTypeResult.User -> \"user-${item.mid}\"\n        is SearchTypeResult.LiveRoom -> \"live-${item.roomId}\"\n        else -> item.hashCode()\n    }\n}\n\n@Composable\nprivate fun SearchResultListItem(\n    modifier: Modifier = Modifier,\n    searchResult: SearchTypeResult.SearchTypeResultItem,\n    onClick: () -> Unit,\n    onLongClick: () -> Unit,\n    onFocus: () -> Unit\n) {\n    when (searchResult) {\n        is SearchTypeResult.Video -> {\n            SmallVideoCard(\n                modifier = modifier,\n                data = VideoCardData(\n                    avid = searchResult.aid,\n                    title = searchResult.title.removeHtmlTags(),\n                    cover = searchResult.cover,\n                    play = with(searchResult.play) { if (this == -1L) null else this },\n                    danmaku = with(searchResult.danmaku) { if (this == -1) null else this },\n                    upName = searchResult.author,\n                    time = searchResult.duration * 1000L,\n                    pubTime = searchResult.pubTime.toLong().toSmartDate()\n                ),\n                onClick = onClick,\n                onLongClick = onLongClick,\n                onFocus = onFocus\n            )\n        }\n\n        is SearchTypeResult.Pgc -> {\n            SeasonCard(\n                modifier = modifier,\n                data = SeasonCardData(\n                    seasonId = searchResult.seasonId,\n                    title = searchResult.title.removeHtmlTags(),\n                    cover = searchResult.cover,\n                    rating = String.format(\"%.1f\", searchResult.star)\n                ),\n                onClick = onClick,\n                onLongClick = onLongClick,\n                onFocus = onFocus\n            )\n        }\n\n        is SearchTypeResult.User -> {\n            UpCard(\n                modifier = modifier.focusedScale(0.95f),\n                face = searchResult.avatar,\n                sign = searchResult.sign,\n                username = searchResult.name,\n                onFocusChange = { if (it) onFocus() },\n                onClick = onClick,\n                onLongClick = onLongClick\n            )\n        }\n\n        is SearchTypeResult.LiveRoom -> {\n            LiveRoomCard(\n                modifier = modifier,\n                data = LiveRoomItem(\n                    roomId = searchResult.roomId.toInt(),\n                    uid = 0, // Not available directly in search result\n                    title = searchResult.title.removeHtmlTags(),\n                    uname = searchResult.uname,\n                    online = searchResult.online,\n                    userCover = \"\",\n                    systemCover = \"\",\n                    cover = searchResult.cover,\n                    face = searchResult.face,\n                    parentId = 0,\n                    parentName = \"\",\n                    areaId = 0,\n                    areaName = searchResult.areaName ?: \"\"\n                ),\n                onClick = onClick\n            )\n        }\n\n        else -> {\n\n        }\n    }\n}\n\nfun SearchType.getDisplayName(context: Context) = when (this) {\n    SearchType.Video -> context.getString(R.string.search_result_type_name_video)\n    SearchType.MediaBangumi -> context.getString(R.string.search_result_type_name_media_bangumi)\n    SearchType.MediaFt -> context.getString(R.string.search_result_type_name_media_ft)\n    SearchType.BiliUser -> context.getString(R.string.search_result_type_name_bili_user)\n    SearchType.LiveRoom -> context.getString(R.string.search_result_type_name_live_room)\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/LogsScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings\n\nimport android.content.Context\nimport android.content.res.Configuration\nimport android.net.ConnectivityManager\nimport android.net.NetworkCapabilities\nimport android.net.wifi.WifiManager\nimport android.os.Build\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.ListItem\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.component.QrImage\nimport dev.aaa1115910.bv.network.HttpServer\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.LogCatcherUtil\nimport dev.aaa1115910.bv.util.swapList\nimport dev.aaa1115910.bv.util.toast\nimport java.io.File\nimport java.net.Inet4Address\nimport java.net.NetworkInterface\n\n@Composable\nfun LogsScreen(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n\n    var host by remember { mutableStateOf(\"x.x.x.x\") }\n    var port by remember { mutableIntStateOf(0) }\n\n    val logs = remember { mutableStateListOf<File>() }\n    var currentSelectFile by remember { mutableStateOf<File?>(null) }\n\n    var qrContent by remember { mutableStateOf(\"\") }\n\n    val updateQRCode = {\n        val url = \"http://$host:$port/api/logs/${currentSelectFile?.name}\"\n        qrContent = url\n    }\n\n    @Suppress(\"DEPRECATION\")\n    val getIpAddress: () -> String = {\n        val connectivityManager =\n            context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n        val isWifi: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            val s = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)\n            s?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true\n        } else {\n            val networkInfo = connectivityManager.activeNetworkInfo\n            networkInfo?.type == ConnectivityManager.TYPE_WIFI\n        }\n\n        var ip = \"\"\n        if (isWifi) {\n            val wifiManager =\n                context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager\n            val wifiInfo = wifiManager.connectionInfo\n            val ipNum = wifiInfo.ipAddress\n            ip =\n                \"${ipNum and 0xFF}.${ipNum shr 8 and 0xFF}.${ipNum shr 16 and 0xFF}.${ipNum shr 24 and 0xFF}\"\n        } else {\n            val en = NetworkInterface.getNetworkInterfaces()\n            while (en.hasMoreElements()) {\n                val intf = en.nextElement()\n                val enumIpAddr = intf.inetAddresses\n                while (ip == \"\" && enumIpAddr.hasMoreElements()) {\n                    val inetAddress = enumIpAddr.nextElement()\n                    if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) {\n                        ip = inetAddress.getHostAddress() ?: \"\"\n                        break\n                    }\n                }\n            }\n        }\n        ip\n    }\n\n    val updateLogs = {\n        LogCatcherUtil.updateLogFiles()\n        val newLogs = (LogCatcherUtil.manualFiles + LogCatcherUtil.crashFiles)\n            .sortedByDescending { it.lastModified() }\n        logs.swapList(newLogs)\n    }\n\n    LaunchedEffect(Unit) {\n        host = getIpAddress()\n        port = HttpServer.server?.engine?.resolvedConnectors()?.first()?.port ?: 0\n\n        updateLogs()\n    }\n\n    LogsScreenContent(\n        modifier = modifier,\n        qrContent = qrContent,\n        clearQrContent = { qrContent = \"\" },\n        logs = logs,\n        onFocusLogFile = { file ->\n            currentSelectFile = file\n            updateQRCode()\n        },\n        onClickCreateLog = {\n            LogCatcherUtil.logLogcat(manual = true)\n            \"Log created\".toast(context)\n            updateLogs()\n        }\n    )\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun LogsScreenContent(\n    modifier: Modifier = Modifier,\n    qrContent: String,\n    clearQrContent: () -> Unit,\n    logs: List<File>,\n    onFocusLogFile: (File) -> Unit,\n    onClickCreateLog: () -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus()\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            Box(\n                modifier = Modifier.padding(start = 48.dp, top = 24.dp, bottom = 8.dp, end = 48.dp)\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.Bottom,\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Text(\n                        text = stringResource(id = R.string.title_activity_logs),\n                        fontSize = 48.sp\n                    )\n                }\n            }\n        }\n    ) { innerPadding ->\n        Row(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(innerPadding)\n        ) {\n            Box(\n                modifier = Modifier.weight(1f)\n            ) {\n                LazyColumn(\n                    modifier = Modifier.fillMaxSize(),\n                    contentPadding = PaddingValues(\n                        horizontal = 36.dp,\n                        vertical = 12.dp\n                    )\n                ) {\n                    item {\n                        CreateLogItem(\n                            modifier = Modifier.focusRequester(focusRequester),\n                            onFocus = clearQrContent,\n                            onClick = onClickCreateLog\n                        )\n                    }\n                    itemsIndexed(\n                        items = logs,\n                        key = { index, logFile -> \"$index-${logFile.absolutePath}\" }\n                    ) { _, logFile ->\n                        LogItem(\n                            filename = logFile.name,\n                            size = logFile.length(),\n                            onFocus = { onFocusLogFile(logFile) }\n                        )\n                    }\n                    if (logs.isEmpty()) {\n                        item {\n                            Box(\n                                modifier = Modifier\n                                    .fillMaxWidth()\n                                    .height(200.dp),\n                                contentAlignment = Alignment.Center\n                            ) {\n                                Text(text = stringResource(R.string.log_list_empty))\n                            }\n                        }\n                    }\n                }\n            }\n            Box(\n                modifier = Modifier\n                    .weight(1f)\n                    .fillMaxSize(),\n                contentAlignment = Alignment.Center\n            ) {\n                if (qrContent.isNotBlank()) {\n                    QrImage(\n                        modifier = Modifier.size(240.dp),\n                        content = qrContent,\n                        showLoadingWhenContentChanged = false\n                    )\n                } else {\n                    Text(\n                        text = stringResource(R.string.log_qr_code_empty),\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun LogItem(\n    modifier: Modifier = Modifier,\n    filename: String,\n    size: Long,\n    onFocus: () -> Unit\n) {\n    ListItem(\n        modifier = modifier\n            .onFocusChanged {\n                if (it.hasFocus) onFocus()\n            },\n        selected = false,\n        onClick = { /*TODO*/ },\n        headlineContent = {\n            Text(text = filename)\n        },\n        supportingContent = {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.SpaceBetween\n            ) {\n                Text(\n                    text = if (filename.startsWith(\"logs_manual\"))\n                        stringResource(R.string.log_type_manual)\n                    else\n                        stringResource(R.string.log_type_crash)\n                )\n                Text(\n                    text = \"${size / 1024} KB\"\n                )\n            }\n        }\n    )\n}\n\n@Composable\nfun CreateLogItem(\n    modifier: Modifier = Modifier,\n    onFocus: () -> Unit,\n    onClick: () -> Unit,\n) {\n    ListItem(\n        modifier = modifier\n            .onFocusChanged {\n                if (it.hasFocus) onFocus()\n            },\n        selected = false,\n        onClick = onClick,\n        headlineContent = {\n            Text(text = stringResource(R.string.log_save_now_button))\n        }\n    )\n}\n\n@Preview\n@Composable\nfun LogItemPreview() {\n    BVTheme {\n        LogItem(\n            filename = \"logs_manual_3202-11-11_08:16:23.log\",\n            size = 2145,\n            onFocus = {}\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun LogsScreenContentPreview() {\n    BVTheme {\n        LogsScreenContent(\n            qrContent = \"\",\n            clearQrContent = {},\n            logs = listOf(\n                File(\"logs_manual_3202-11-11_08:16:23.log\"),\n                File(\"logs_crash_3202-11-11_08:16:23.log\")\n            ),\n            onFocusLogFile = {},\n            onClickCreateLog = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/MediaCodecScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings\n\nimport android.os.Build\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Audiotrack\nimport androidx.compose.material.icons.filled.Videocam\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.nativeKeyCode\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.ListItem\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.CodecInfoData\nimport dev.aaa1115910.bv.util.CodecMedia\nimport dev.aaa1115910.bv.util.CodecMode\nimport dev.aaa1115910.bv.util.CodecType\nimport dev.aaa1115910.bv.util.CodecUtil\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.util.swapList\nimport java.util.Locale\n\n@Composable\nfun MediaCodecScreen(\n    modifier: Modifier = Modifier\n) {\n    val showLargeTitle by remember { derivedStateOf { true } }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n\n    var currentCodecInfoData by remember { mutableStateOf<CodecInfoData?>(null) }\n    var focusInNav by remember { mutableStateOf(false) }\n\n    val decoderList = remember { mutableStateListOf<CodecInfoData>() }\n\n    LaunchedEffect(Unit) {\n        val list = CodecUtil.parseCodecs().filter { it.type == CodecType.Decoder }\n        decoderList.swapList(list)\n        currentCodecInfoData = list.firstOrNull()\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            Box(\n                modifier = Modifier.padding(\n                    start = 48.dp,\n                    top = 24.dp,\n                    bottom = 8.dp,\n                    end = 48.dp\n                )\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.Bottom,\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Text(\n                        text = stringResource(R.string.title_activity_media_codec),\n                        fontSize = titleFontSize.sp\n                    )\n                    Text(\n                        text = \"\",\n                        color = Color.White.copy(alpha = 0.6f)\n                    )\n                }\n            }\n        }\n    ) { innerPadding ->\n        Row(\n            modifier = Modifier.padding(innerPadding)\n        ) {\n            MediaCodecListItems(\n                modifier = Modifier\n                    .onFocusChanged { focusInNav = it.hasFocus }\n                    .weight(3f)\n                    .fillMaxHeight(),\n                codecInfoDataList = decoderList,\n                currentCodecInfoData = currentCodecInfoData,\n                onCodecInfoDataChanged = { currentCodecInfoData = it },\n                isFocusing = focusInNav\n            )\n            MediaCodecDetails(\n                modifier = Modifier\n                    .weight(5f)\n                    .fillMaxSize(),\n                onBackNav = { focusInNav = true },\n                currentCodecInfoData = currentCodecInfoData\n            )\n        }\n    }\n}\n\n@Composable\nfun MediaCodecListItems(\n    modifier: Modifier = Modifier,\n    codecInfoDataList: List<CodecInfoData>,\n    currentCodecInfoData: CodecInfoData?,\n    onCodecInfoDataChanged: (CodecInfoData) -> Unit,\n    isFocusing: Boolean\n) {\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(isFocusing) {\n        if (isFocusing && codecInfoDataList.isNotEmpty()) focusRequester.requestFocus(scope)\n    }\n\n    LaunchedEffect(codecInfoDataList) {\n        if (codecInfoDataList.isNotEmpty()) focusRequester.requestFocus(scope)\n    }\n\n    LazyColumn(\n        modifier = modifier,\n        contentPadding = PaddingValues(24.dp),\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        itemsIndexed(\n            items = codecInfoDataList,\n            key = { index, codecInfoData -> \"$index-$codecInfoData\" }\n        ) { _, codecInfoData ->\n            val buttonModifier = if (currentCodecInfoData == codecInfoData) Modifier\n                .focusRequester(focusRequester)\n                .fillMaxWidth()\n            else Modifier.fillMaxWidth()\n            MediaCodecListItem(\n                modifier = buttonModifier,\n                codecInfoData = codecInfoData,\n                onFocus = { onCodecInfoDataChanged(codecInfoData) },\n                selected = currentCodecInfoData == codecInfoData\n            )\n        }\n    }\n}\n\n@Composable\nfun MediaCodecListItem(\n    modifier: Modifier = Modifier,\n    codecInfoData: CodecInfoData,\n    onFocus: () -> Unit,\n    onLoseFocus: () -> Unit = {},\n    onClick: () -> Unit = {},\n    selected: Boolean\n) {\n    ListItem(\n        modifier = modifier\n            .onFocusChanged { if (it.hasFocus) onFocus() else onLoseFocus() },\n        selected = selected,\n        onClick = onClick,\n        headlineContent = {\n            Text(\n                modifier = Modifier.padding(horizontal = 16.dp),\n                text = codecInfoData.name,\n                //style = MaterialTheme.typography.titleLarge\n            )\n        },\n        overlineContent = {\n            Row(\n                modifier = Modifier.padding(horizontal = 16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                Text(\n                    modifier = Modifier\n                        .clip(MaterialTheme.shapes.small)\n                        .background(MaterialTheme.colorScheme.surfaceVariant)\n                        .padding(horizontal = 8.dp),\n                    text = codecInfoData.mimeType,\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n                Icon(\n                    imageVector = when (codecInfoData.media) {\n                        CodecMedia.Audio -> Icons.Default.Audiotrack\n                        CodecMedia.Video -> Icons.Default.Videocam\n                    }, contentDescription = null\n                )\n            }\n        }\n    )\n}\n\n@Composable\nfun MediaCodecDetails(\n    modifier: Modifier = Modifier,\n    onBackNav: () -> Unit,\n    currentCodecInfoData: CodecInfoData?\n) {\n    val context = LocalContext.current\n\n    if (currentCodecInfoData != null) {\n        LazyColumn(\n            modifier = modifier\n                .fillMaxSize()\n                .onPreviewKeyEvent {\n                    val result = it.key.nativeKeyCode == android.view.KeyEvent.KEYCODE_DPAD_LEFT\n                    if (result) onBackNav()\n                    result\n                },\n            verticalArrangement = Arrangement.spacedBy(12.dp),\n            contentPadding = PaddingValues(\n                horizontal = 48.dp,\n                vertical = 24.dp\n            )\n        ) {\n            item {\n                MediaCodecDetailItem(\n                    title = stringResource(R.string.codec_detail_hs_title),\n                    text = when (currentCodecInfoData.mode) {\n                        CodecMode.Hardware -> stringResource(R.string.codec_detail_hs_hardware)\n                        CodecMode.Software -> stringResource(R.string.codec_detail_hs_software)\n                    }\n                )\n            }\n            item {\n                MediaCodecDetailItem(\n                    title = stringResource(R.string.codec_detail_max_supported_instances_title),\n                    text = currentCodecInfoData.maxSupportedInstances.toString()\n                )\n            }\n            if (currentCodecInfoData.media == CodecMedia.Video) {\n                item {\n                    MediaCodecDetailItem(\n                        title = stringResource(R.string.codec_detail_color_formats_title),\n                        text = currentCodecInfoData.colorFormats.joinToString()\n                    )\n                }\n            }\n            if (currentCodecInfoData.media == CodecMedia.Audio) {\n                item {\n                    MediaCodecDetailItem(\n                        title = stringResource(R.string.codec_detail_audio_bitrate_range_title),\n                        text = \"${currentCodecInfoData.audioBitrateRange?.first?.toBps()} - ${currentCodecInfoData.audioBitrateRange?.last?.toBps()}\"\n                    )\n                }\n            }\n            if (currentCodecInfoData.media == CodecMedia.Video) {\n                item {\n                    MediaCodecDetailItem(\n                        title = stringResource(R.string.codec_detail_video_max_bitrate_title),\n                        text = currentCodecInfoData.videoBitrateRange?.last?.toBps() ?: \"Unknown\"\n                    )\n                }\n            }\n            if (currentCodecInfoData.media == CodecMedia.Video) {\n                item {\n                    MediaCodecDetailItem(\n                        title = stringResource(R.string.codec_detail_video_frame_range_title),\n                        text = \"${currentCodecInfoData.videoFrame?.first}fps - ${currentCodecInfoData.videoFrame?.last}fps\"\n                    )\n                }\n            }\n            if (currentCodecInfoData.media == CodecMedia.Video) {\n                item {\n                    MediaCodecDetailItem(\n                        title = stringResource(R.string.codec_detail_video_frame_supported_title),\n                        text = currentCodecInfoData.supportedFrameRates.joinToString(\"\\n\") { supportedFrameRate ->\n                            when (supportedFrameRate.resolution.second) {\n                                360 -> context.getString(R.string.codec_detail_video_resolution_360p)\n                                480 -> context.getString(R.string.codec_detail_video_resolution_480p)\n                                720 -> context.getString(R.string.codec_detail_video_resolution_720p)\n                                1080 -> context.getString(R.string.codec_detail_video_resolution_1080p)\n                                1440 -> context.getString(R.string.codec_detail_video_resolution_1440p)\n                                2160 -> context.getString(R.string.codec_detail_video_resolution_2160p)\n                                4320 -> context.getString(R.string.codec_detail_video_resolution_4320p)\n                                else -> context.getString(R.string.codec_detail_video_resolution_unknown)\n                            } + \": \" +\n                                    (\"${\n                                        String.format(\n                                            Locale.getDefault(),\n                                            \"%.1f\",\n                                            supportedFrameRate.frameRate.upper\n                                        )\n                                    }fps\"\n                                        .takeUnless { supportedFrameRate.unsupported }\n                                        ?: context.getString(R.string.codec_detail_video_frame_unsupported))\n                        }\n                    )\n                }\n            }\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && currentCodecInfoData.media == CodecMedia.Video) {\n                item {\n                    MediaCodecDetailItem(\n                        title = stringResource(R.string.codec_detail_video_frame_achievable_title),\n                        text = currentCodecInfoData.achievableFrameRates.joinToString(\"\\n\") { achievableFrameRates ->\n                            when (achievableFrameRates.resolution.second) {\n                                360 -> context.getString(R.string.codec_detail_video_resolution_360p)\n                                480 -> context.getString(R.string.codec_detail_video_resolution_480p)\n                                720 -> context.getString(R.string.codec_detail_video_resolution_720p)\n                                1080 -> context.getString(R.string.codec_detail_video_resolution_1080p)\n                                1440 -> context.getString(R.string.codec_detail_video_resolution_1440p)\n                                2160 -> context.getString(R.string.codec_detail_video_resolution_2160p)\n                                4320 -> context.getString(R.string.codec_detail_video_resolution_4320p)\n                                else -> context.getString(R.string.codec_detail_video_resolution_unknown)\n                            } + \": \" +\n                                    (\"${\n                                        String.format(\n                                            Locale.getDefault(),\n                                            \"%.1f\",\n                                            achievableFrameRates.frameRate.upper\n                                        )\n                                    }fps\"\n                                        .takeUnless { achievableFrameRates.unsupported }\n                                        ?: context.getString(R.string.codec_detail_video_frame_unsupported))\n                        }\n                    )\n                }\n            }\n        }\n    } else {\n        Box(\n            modifier = modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(text = stringResource(R.string.codec_list_empty))\n        }\n    }\n}\n\n@Composable\nfun MediaCodecDetailItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    text: String\n) {\n    var hasFocus by remember { mutableStateOf(false) }\n\n    ListItem(\n        modifier = modifier\n            .onFocusChanged { hasFocus = it.hasFocus },\n        selected = hasFocus,\n        onClick = {},\n        headlineContent = { Text(text = title) },\n        supportingContent = { Text(text = text) }\n    )\n}\n\nprivate val previewCodecInfoData = CodecInfoData(\n    name = \"c2.android.avc.decoder\",\n    type = CodecType.Decoder,\n    mode = CodecMode.Hardware,\n    media = CodecMedia.Video,\n    mimeType = \"video/avc\",\n    maxSupportedInstances = 1,\n    colorFormats = listOf(21, 19, 20),\n    audioBitrateRange = 0..0,\n    videoBitrateRange = 0..0,\n    videoFrame = 0..0,\n    supportedFrameRates = emptyList(),\n    achievableFrameRates = emptyList()\n)\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun MediaCodecListItemPreview() {\n    BVTheme {\n        MediaCodecListItem(\n            codecInfoData = previewCodecInfoData,\n            onFocus = {},\n            selected = false\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun MediaCodecDetailsPreview() {\n    BVTheme {\n        MediaCodecDetails(\n            currentCodecInfoData = previewCodecInfoData,\n            onBackNav = {}\n        )\n    }\n}\n\nprivate fun Int.toBps(): String {\n    return when {\n        this >= 1000000 -> \"${this / 1000000} Mbps\"\n        this >= 1000 -> \"${this / 1000} Kbps\"\n        else -> \"$this bps\"\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/SettingsScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings\n\nimport android.content.Context\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.nativeKeyCode\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.ListItem\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.screens.settings.content.AboutSetting\nimport dev.aaa1115910.bv.tv.screens.settings.content.InfoSetting\nimport dev.aaa1115910.bv.tv.screens.settings.content.NetworkSetting\nimport dev.aaa1115910.bv.tv.screens.settings.content.OtherSetting\nimport dev.aaa1115910.bv.tv.screens.settings.content.PlayerSetting\nimport dev.aaa1115910.bv.tv.screens.settings.content.PlayerTypeSetting\nimport dev.aaa1115910.bv.tv.screens.settings.content.StorageSetting\nimport dev.aaa1115910.bv.tv.screens.settings.content.UISetting\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.requestFocus\n\n@Composable\nfun SettingsScreen(\n    modifier: Modifier = Modifier\n) {\n    val showLargeTitle by remember { derivedStateOf { true } }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n\n    var currentMenu by remember { mutableStateOf(SettingsMenuNavItem.Player) }\n    var focusInNav by remember { mutableStateOf(false) }\n\n    Scaffold(\n        modifier = modifier,\n    ) { innerPadding ->\n        Row(\n            modifier = Modifier.padding(innerPadding)\n        ) {\n            Column(\n                modifier = Modifier\n                    .weight(3f)\n                    .fillMaxHeight()\n            ) {\n                Text(\n                    modifier = Modifier.padding(\n                        start = 48.dp,\n                        top = 24.dp,\n                        bottom = 8.dp,\n                        end = 48.dp\n                    ),\n                    text = stringResource(R.string.title_activity_settings),\n                    fontSize = titleFontSize.sp\n                )\n                SettingsNav(\n                    modifier = Modifier\n                        .onFocusChanged { focusInNav = it.hasFocus },\n                    currentMenu = currentMenu,\n                    onMenuChanged = { currentMenu = it },\n                    isFocusing = focusInNav\n                )\n            }\n            SettingContent(\n                modifier = Modifier\n                    .weight(5f)\n                    .fillMaxSize()\n                    .padding(top = 48.dp),\n                onBackNav = { focusInNav = true },\n                currentMenu = currentMenu\n            )\n        }\n    }\n}\n\n@Composable\nfun SettingsNav(\n    modifier: Modifier = Modifier,\n    currentMenu: SettingsMenuNavItem,\n    onMenuChanged: (SettingsMenuNavItem) -> Unit,\n    isFocusing: Boolean\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(isFocusing) {\n        if (isFocusing) focusRequester.requestFocus(scope)\n    }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus(scope)\n    }\n\n    LazyColumn(\n        modifier = modifier,\n        contentPadding = PaddingValues(24.dp),\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        for (item in SettingsMenuNavItem.entries - listOf(SettingsMenuNavItem.PlayerType)) {\n            val buttonModifier = if (currentMenu == item) Modifier\n                .focusRequester(focusRequester)\n                .fillMaxWidth()\n            else Modifier.fillMaxWidth()\n            item {\n                SettingsMenuButton(\n                    modifier = buttonModifier,\n                    text = item.getDisplayName(context),\n                    selected = currentMenu == item,\n                    onFocus = {\n                        onMenuChanged(item)\n                    }\n                )\n            }\n        }\n    }\n}\n\nenum class SettingsMenuNavItem(private val strRes: Int) {\n    Player(R.string.settings_item_player),\n    PlayerType(R.string.settings_item_player_type),\n    UI(R.string.settings_item_ui),\n    Other(R.string.settings_item_other),\n    Storage(R.string.settings_item_storage),\n    Network(R.string.settings_item_network),\n    Info(R.string.settings_item_info),\n    About(R.string.settings_item_about);\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n}\n\n@Composable\nfun SettingContent(\n    modifier: Modifier = Modifier,\n    onBackNav: () -> Unit,\n    currentMenu: SettingsMenuNavItem\n) {\n    Box(\n        modifier = modifier\n            .padding(24.dp)\n    ) {\n        SettingsDetail(\n            modifier = Modifier.fillMaxSize(),\n            onFocusBackMenuList = {\n                onBackNav()\n            }\n        ) {\n            when (currentMenu) {\n                SettingsMenuNavItem.Player -> PlayerSetting()\n                SettingsMenuNavItem.Info -> InfoSetting()\n                SettingsMenuNavItem.About -> AboutSetting()\n                SettingsMenuNavItem.Other -> OtherSetting()\n                SettingsMenuNavItem.Network -> NetworkSetting()\n                SettingsMenuNavItem.PlayerType -> PlayerTypeSetting()\n                SettingsMenuNavItem.UI -> UISetting()\n                SettingsMenuNavItem.Storage -> StorageSetting()\n            }\n        }\n    }\n}\n\n@Composable\nfun SettingsMenuButton(\n    modifier: Modifier = Modifier,\n    text: String,\n    onFocus: () -> Unit,\n    onLoseFocus: () -> Unit = {},\n    onClick: () -> Unit = {},\n    selected: Boolean\n) {\n    ListItem(\n        modifier = modifier\n            .onFocusChanged { if (it.hasFocus) onFocus() else onLoseFocus() },\n        selected = selected,\n        onClick = onClick,\n        headlineContent = {\n            Text(\n                modifier = Modifier.padding(\n                    horizontal = 16.dp\n                ),\n                text = text,\n                style = MaterialTheme.typography.titleLarge\n            )\n        }\n    )\n}\n\n@Preview\n@Composable\nfun SettingsMenuButtonPreview() {\n    BVTheme {\n        Box(\n            modifier = Modifier.size(200.dp, 100.dp)\n        ) {\n            SettingsMenuButton(\n                modifier = Modifier.align(Alignment.Center),\n                text = \"This is button\",\n                selected = true,\n                onFocus = {}\n            )\n        }\n    }\n}\n\n@Composable\nfun SettingsDetail(\n    modifier: Modifier = Modifier,\n    onFocusBackMenuList: () -> Unit,\n    content: @Composable () -> Unit\n) {\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n            .onPreviewKeyEvent {\n                val result = it.key.nativeKeyCode == android.view.KeyEvent.KEYCODE_DPAD_LEFT\n                if (result) onFocusBackMenuList()\n                result\n            }\n    ) {\n        content()\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/SpeedTestScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings\n\nimport android.util.Base64\nimport android.webkit.CookieManager\nimport android.webkit.WebView\nimport android.webkit.WebView.setWebContentsDebuggingEnabled\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.SurfaceDefaults\nimport androidx.tv.material3.Text\nimport androidx.webkit.WebViewClientCompat\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.Prefs\n\n@Composable\nfun SpeedTestScreen(\n    modifier: Modifier = Modifier\n) {\n    var loading by remember { mutableStateOf(true) }\n\n    BoxWithConstraints {\n        val width = with(LocalDensity.current) {\n            this@BoxWithConstraints.maxWidth.toPx().toInt()\n        }\n\n        val webViewClient = object : WebViewClientCompat() {\n            override fun onPageFinished(view: WebView?, url: String?) {\n                super.onPageFinished(view, url)\n\n                //默认的 css 会无法正常显示\n                val css = \"\"\"\n                .container {\n                    width: 1920px !important;\n                    height: 1080px !important;\n                }\n            \"\"\".trimIndent()\n                val encoded: String = Base64.encodeToString(css.toByteArray(), Base64.NO_WRAP)\n\n                view?.loadUrl(\n                    \"javascript:(function() {\" +\n                            \"var parent = document.getElementsByTagName('head').item(0);\" +\n                            \"var style = document.createElement('style');\" +\n                            \"style.type = 'text/css';\" +\n                            // Tell the browser to BASE64-decode the string into your script !!!\n                            \"style.innerHTML = window.atob('\" + encoded + \"');\" +\n                            \"parent.appendChild(style)\" +\n                            \"})()\"\n                )\n                //处理完css还得处理缩放\n                view?.setInitialScale(((width / 1920f) * 100).toInt())\n                loading = false\n            }\n        }\n\n        CookieManager.getInstance().apply {\n            val cookies = mapOf(\n                \"DedeUserID\" to Prefs.uid,\n                \"DedeUserID__ckMd5\" to Prefs.uidCkMd5,\n                \"SESSDATA\" to Prefs.sessData,\n                \"bili_jct\" to Prefs.biliJct,\n                \"sid\" to Prefs.sid\n            )\n\n            cookies.forEach { (name, value) ->\n                setCookie(\".bilibili.com\", \"$name=$value\")\n            }\n        }\n\n        AndroidView(\n            modifier = modifier.fillMaxSize(),\n            factory = { ctx ->\n                WebView(ctx).apply {\n                    this.webViewClient = webViewClient\n\n                    setWebContentsDebuggingEnabled(true)\n\n                    settings.apply {\n                        userAgentString =\n                            dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_WEB\n                        javaScriptEnabled = true\n                    }\n\n                    loadUrl(\"https://www.bilibili.com/blackboard/video-diagnostics.html\")\n                }\n            }\n        )\n\n        if (loading) {\n            Surface(\n                modifier = Modifier.fillMaxSize(),\n                colors = SurfaceDefaults.colors(\n                    containerColor = Color.Black.copy(alpha = 0.9f)\n                )\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center\n                ) {\n                    Text(text = stringResource(R.string.loading))\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/content/AboutSetting.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings.content\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.network.GithubApi\nimport dev.aaa1115910.bv.tv.component.settings.UpdateDialog\nimport dev.aaa1115910.bv.tv.screens.settings.SettingsMenuNavItem\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.fException\nimport dev.aaa1115910.bv.util.fInfo\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\n@Composable\nfun AboutSetting(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n    val logger = KotlinLogging.logger(\"AboutSetting\")\n\n    var showUpdateDialog by remember { mutableStateOf(false) }\n    var latestVersionName by remember { mutableStateOf(\"Loading...\") }\n\n    LaunchedEffect(Unit) {\n        launch(Dispatchers.IO) {\n            runCatching {\n                latestVersionName = GithubApi.getLatestBuild().name\n                if (latestVersionName.isEmpty()) {\n                    latestVersionName = GithubApi.getLatestBuild().tagName\n                }\n\n                logger.fInfo { \"Find latest version $latestVersionName\" }\n            }.onFailure {\n                logger.fException(it) { \"Failed to get latest version\" }\n                latestVersionName = \"Error\"\n            }\n        }\n    }\n\n    Box(\n        modifier = modifier\n    ) {\n        Column(\n            modifier = Modifier.fillMaxSize(),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            Text(\n                text = SettingsMenuNavItem.About.getDisplayName(context),\n                style = MaterialTheme.typography.displaySmall\n            )\n            Spacer(modifier = Modifier.height(12.dp))\n            Column(\n                verticalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                Text(\n                    text = stringResource(R.string.about_statement),\n                    style = MaterialTheme.typography.titleMedium,\n                    color = Color.Red\n                )\n                Text(\n                    text = stringResource(\n                        R.string.settings_version_current_version,\n                        \"${BuildConfig.VERSION_NAME}.${BuildConfig.BUILD_TYPE}\"\n                    )\n                )\n                Row(\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Text(\n                        text = stringResource(\n                            R.string.settings_version_latest_version,\n                            latestVersionName\n                        )\n                    )\n                }\n            }\n            Button(onClick = { showUpdateDialog = true }) {\n                Text(text = stringResource(R.string.settings_version_check_update_button))\n            }\n        }\n        Text(\n         modifier = Modifier.align(Alignment.BottomCenter),\n         text = \"https://github.com/fantasytyx/bv\"\n        )\n    }\n\n    UpdateDialog(\n        show = showUpdateDialog,\n        onHideDialog = { showUpdateDialog = false }\n    )\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun AboutSettingPreview() {\n    BVTheme {\n        AboutSetting()\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/content/InfoSetting.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings.content\n\nimport android.app.Activity\nimport android.app.ActivityManager\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport android.os.Environment\nimport android.os.StatFs\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.activities.settings.MediaCodecActivity\nimport dev.aaa1115910.bv.tv.screens.settings.SettingsMenuNavItem\nimport java.text.DecimalFormat\nimport kotlin.math.pow\n\n\n@Composable\nfun InfoSetting(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n\n    val memoryInfo by remember {\n        mutableStateOf(\n            lazy {\n                runCatching {\n                    val memoryInfo = ActivityManager.MemoryInfo()\n                    (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager)\n                        .getMemoryInfo(memoryInfo)\n                    val df = DecimalFormat(\"###.##\")\n                    Pair(\n                        \"${df.format(memoryInfo.availMem / 1024.0.pow(3))} GB\",\n                        \"${df.format(memoryInfo.totalMem / 1024.0.pow(3))} GB\"\n                    )\n                }.getOrDefault(Pair(\"Unknown\", \"Unknown\"))\n            }.value\n        )\n    }\n\n    val storageInfo by remember {\n        mutableStateOf(\n            lazy {\n                runCatching {\n                    val statFs = StatFs(Environment.getExternalStorageDirectory().absolutePath)\n                    val df = DecimalFormat(\"###.##\")\n                    Pair(\n                        \"${df.format(statFs.availableBytes / 1024.0.pow(3))} GB\",\n                        \"${df.format(statFs.totalBytes / 1024.0.pow(3))} GB\"\n                    )\n                }.getOrDefault(Pair(\"Unknown\", \"Unknown\"))\n            }.value\n        )\n    }\n\n    @Suppress(\"DEPRECATION\")\n    val screenInfo by remember {\n        mutableStateOf(\n            lazy {\n                val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n                    context.display!!\n                } else {\n                    (context as Activity).windowManager.defaultDisplay\n                }\n\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n                    val mode = display.mode\n                    Triple(mode.physicalWidth, mode.physicalHeight, mode.refreshRate)\n                } else {\n                    Triple(display.width, display.height, display.refreshRate)\n                }\n            }.value\n        )\n    }\n\n\n\n    Column(\n        modifier = modifier.fillMaxSize(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(12.dp)\n    ) {\n        Text(\n            text = SettingsMenuNavItem.Info.getDisplayName(context),\n            style = MaterialTheme.typography.displaySmall\n        )\n        Spacer(modifier = Modifier.height(12.dp))\n        Column(\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            Text(text = stringResource(R.string.settings_info_manufacturer, Build.MANUFACTURER))\n            Text(\n                text = stringResource(R.string.settings_info_model, Build.MODEL, Build.PRODUCT)\n            )\n            Text(text = stringResource(R.string.settings_info_system, Build.VERSION.RELEASE))\n            Text(\n                text = stringResource(\n                    R.string.settings_info_screen,\n                    *arrayOf<Any>(screenInfo.first, screenInfo.second, screenInfo.third)\n                )\n            )\n            if (Build.VERSION.SDK_INT >= 31)\n                Text(\n                    text = stringResource(\n                        R.string.settings_info_soc, Build.SOC_MANUFACTURER, Build.SOC_MODEL\n                    )\n                )\n            Text(\n                text = stringResource(\n                    R.string.settings_info_memory,\n                    *memoryInfo.toList().toTypedArray()\n                )\n            )\n            Text(\n                text = stringResource(\n                    R.string.settings_info_storage,\n                    *storageInfo.toList().toTypedArray()\n                )\n            )\n        }\n        Button(onClick = {\n            context.startActivity(Intent(context, MediaCodecActivity::class.java))\n        }) {\n            Text(stringResource(id = R.string.title_activity_media_codec))\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/content/NetworkSetting.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings.content\n\nimport android.content.Intent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Info\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.http.BiliHttpProxyApi\nimport dev.aaa1115910.biliapi.http.util.BiliDns\nimport dev.aaa1115910.biliapi.repositories.ChannelRepository\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.bv.BVApp\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.activities.settings.SpeedTestActivity\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.settings.SettingListItem\nimport dev.aaa1115910.bv.tv.component.settings.SettingListItemWithDialog\nimport dev.aaa1115910.bv.tv.component.settings.SettingSwitchListItem\nimport dev.aaa1115910.bv.tv.screens.settings.SettingsMenuNavItem\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.Prefs\nimport org.koin.compose.getKoin\n\n@Composable\nfun NetworkSetting(\n    modifier: Modifier = Modifier,\n    channelRepository: ChannelRepository = getKoin().get()\n) {\n    val context = LocalContext.current\n    var selectedApiType by remember { mutableStateOf(Prefs.apiType) }\n    var enableProxy by remember { mutableStateOf(Prefs.enableProxy) }\n    var proxyHttpServer by remember { mutableStateOf(Prefs.proxyHttpServer) }\n    var proxyGRPCServer by remember { mutableStateOf(Prefs.proxyGRPCServer) }\n    var preferOfficialCdn by remember { mutableStateOf(Prefs.preferOfficialCdn) }\n    var ipv4Only by remember { mutableStateOf(Prefs.ipv4Only) }\n    var showProxyHttpServerEditDialog by remember { mutableStateOf(false) }\n    var showProxyGRPCServerEditDialog by remember { mutableStateOf(false) }\n\n    Box(\n        modifier = modifier\n    ) {\n        Column(\n            modifier = modifier.fillMaxSize(),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            Text(\n                text = SettingsMenuNavItem.Network.getDisplayName(context),\n                style = MaterialTheme.typography.displaySmall\n            )\n            Spacer(modifier = Modifier.height(12.dp))\n\n            LazyColumn(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(horizontal = 48.dp),\n                horizontalAlignment = Alignment.CenterHorizontally,\n                verticalArrangement = Arrangement.spacedBy(12.dp)\n            ) {\n                item {\n                    SettingListItemWithDialog(\n                        title = stringResource(R.string.settings_item_api),\n                        supportText = \"\",\n                        options = ApiType.entries,\n                        getDisplayName = { apiType, _ -> apiType.name },\n                        value = selectedApiType,\n                        onValueChange = {\n                            selectedApiType = it\n                            Prefs.apiType = it\n                        }\n                    )\n                }\n\n                item {\n                    Column {\n                        SettingSwitchListItem(\n                            title = stringResource(R.string.settings_network_enable_proxy_title),\n                            supportText = stringResource(R.string.settings_network_enable_proxy_text),\n                            checked = Prefs.enableProxy,\n                            onCheckedChange = { enable ->\n                                enableProxy = enable\n                                Prefs.enableProxy = enable\n                                if (enable) BVApp.instance?.initProxy()\n                            }\n                        )\n                        AnimatedVisibility(visible = enableProxy) {\n                            Column {\n                                SettingListItem(\n                                    modifier = Modifier.padding(top = 12.dp),\n                                    title = stringResource(R.string.settings_network_proxy_http_server_title),\n                                    supportText = if (proxyHttpServer.isBlank()) stringResource(R.string.settings_network_proxy_server_content_empty) else proxyHttpServer,\n                                    onClick = { showProxyHttpServerEditDialog = true }\n                                )\n                                SettingListItem(\n                                    modifier = Modifier.padding(top = 12.dp),\n                                    title = stringResource(R.string.settings_network_proxy_grpc_server_title),\n                                    supportText = if (proxyGRPCServer.isBlank()) stringResource(R.string.settings_network_proxy_server_content_empty) else proxyGRPCServer,\n                                    onClick = { showProxyGRPCServerEditDialog = true }\n                                )\n                            }\n                        }\n                    }\n                }\n\n                item {\n                    SettingSwitchListItem(\n                        title = stringResource(R.string.settings_network_prefer_official_cdn_title),\n                        supportText = stringResource(R.string.settings_network_prefer_official_cdn_text),\n                        checked = Prefs.preferOfficialCdn,\n                        onCheckedChange = { enable ->\n                            preferOfficialCdn = enable\n                            Prefs.preferOfficialCdn = enable\n                        }\n                    )\n                }\n\n                item {\n                    SettingSwitchListItem(\n                        title = stringResource(R.string.settings_network_ipv4_only_title),\n                        supportText = stringResource(R.string.settings_network_ipv4_only_text),\n                        checked = Prefs.ipv4Only,\n                        onCheckedChange = { enable ->\n                            ipv4Only = enable\n                            Prefs.ipv4Only = enable\n                            BiliDns.ipv4Only = enable\n                        }\n                    )\n                }\n\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_network_test_title),\n                        supportText = stringResource(R.string.settings_network_test_text),\n                        onClick = {\n                            context.startActivity(Intent(context, SpeedTestActivity::class.java))\n                        }\n                    )\n                }\n            }\n        }\n    }\n\n    ProxyServerEditDialog(\n        show = showProxyHttpServerEditDialog,\n        onHideDialog = { showProxyHttpServerEditDialog = false },\n        title = stringResource(R.string.settings_network_proxy_http_server_title),\n        proxyServer = proxyHttpServer,\n        onProxyServerChange = {\n            proxyHttpServer = it\n            Prefs.proxyHttpServer = it\n            BiliHttpProxyApi.createClient(it)\n        }\n    )\n    ProxyServerEditDialog(\n        show = showProxyGRPCServerEditDialog,\n        onHideDialog = { showProxyGRPCServerEditDialog = false },\n        title = stringResource(R.string.settings_network_proxy_grpc_server_title),\n        proxyServer = proxyGRPCServer,\n        onProxyServerChange = {\n            proxyGRPCServer = it\n            Prefs.proxyGRPCServer = it\n            runCatching {\n                channelRepository.initProxyChannel(\n                    accessKey = Prefs.accessToken,\n                    buvid = Prefs.buvid,\n                    proxyServer = it\n                )\n            }\n        }\n    )\n}\n\n@Composable\nfun ProxyServerEditDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    title: String,\n    proxyServer: String,\n    onProxyServerChange: (String) -> Unit\n) {\n    var proxyServerString by remember(show) { mutableStateOf(proxyServer) }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            title = { Text(text = title) },\n            text = {\n                Column(\n                    verticalArrangement = Arrangement.spacedBy(12.dp)\n                ) {\n                    OutlinedTextField(\n                        value = proxyServerString,\n                        onValueChange = { proxyServerString = it },\n                        singleLine = true,\n                        maxLines = 1,\n                        shape = MaterialTheme.shapes.medium,\n                        placeholder = { Text(text = stringResource(R.string.proxy_server_edit_dialog_input_field_label)) }\n                    )\n                    Column(\n                        verticalArrangement = Arrangement.spacedBy(4.dp)\n                    ) {\n                        Icon(imageVector = Icons.Outlined.Info, contentDescription = null)\n                        Text(\n                            text = stringResource(R.string.proxy_server_edit_dialog_warning),\n                            style = MaterialTheme.typography.bodySmall\n                        )\n                    }\n                }\n            },\n            onDismissRequest = onHideDialog,\n            confirmButton = {\n                Button(onClick = {\n                    onProxyServerChange(\n                        proxyServerString\n                            .replace(\"\\n\", \"\")\n                            .replace(\"https://\", \"\")\n                            .replace(\"http://\", \"\")\n                    )\n                    onHideDialog()\n                }) {\n                    Text(text = stringResource(id = R.string.common_confirm))\n                }\n            },\n            dismissButton = {\n                OutlinedButton(onClick = onHideDialog) {\n                    Text(text = stringResource(id = R.string.common_cancel))\n                }\n            }\n        )\n    }\n}\n\n@Preview\n@Composable\nfun ProxyServerEditDialogPreview() {\n    BVTheme {\n        ProxyServerEditDialog(\n            show = true,\n            onHideDialog = {},\n            title = \"title\",\n            proxyServer = \"\",\n            onProxyServerChange = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/content/OtherSetting.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings.content\n\nimport android.content.Intent\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.component.settings.SettingListItem\nimport dev.aaa1115910.bv.tv.component.settings.SettingSwitchListItem\nimport dev.aaa1115910.bv.tv.activities.settings.LogsActivity\nimport dev.aaa1115910.bv.tv.screens.settings.SettingsMenuNavItem\nimport dev.aaa1115910.bv.util.Prefs\n\n@Composable\nfun OtherSetting(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n\n    var showFps by remember { mutableStateOf(Prefs.showFps) }\n    var updateAlpha by remember { mutableStateOf(Prefs.updateAlpha) }\n\n    Column(\n        modifier = modifier.fillMaxSize(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(12.dp)\n    ) {\n        Text(\n            text = SettingsMenuNavItem.Other.getDisplayName(context),\n            style = MaterialTheme.typography.displaySmall\n        )\n        Spacer(modifier = Modifier.height(12.dp))\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(horizontal = 48.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            item {\n                SettingSwitchListItem(\n                    title = stringResource(R.string.settings_other_fps_title),\n                    supportText = stringResource(R.string.settings_other_fps_text),\n                    checked = showFps,\n                    onCheckedChange = {\n                        showFps = it\n                        Prefs.showFps = it\n                    }\n                )\n            }\n            item {\n                SettingSwitchListItem(\n                    title = stringResource(R.string.settings_other_alpha_title),\n                    supportText = stringResource(R.string.settings_other_alpha_text),\n                    checked = updateAlpha,\n                    onCheckedChange = {\n                        updateAlpha = it\n                        Prefs.updateAlpha = it\n                    }\n                )\n            }\n            item {\n                SettingListItem(\n                    title = stringResource(R.string.settings_create_logs_title),\n                    supportText = stringResource(R.string.settings_create_logs_text),\n                    onClick = {\n                        context.startActivity(Intent(context, LogsActivity::class.java))\n                    }\n                )\n            }\n            if (BuildConfig.DEBUG) {\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_crash_test_title),\n                        supportText = stringResource(R.string.settings_crash_test_text),\n                        onClick = {\n                            throw Exception(\"Boom!\")\n                        }\n                    )\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/content/PlayerSetting.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings.content\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableDoubleStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.ListItem\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.RadioButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.PortraitVideoFixMode\nimport dev.aaa1115910.bv.player.entity.PlayerLoadNextAction\nimport dev.aaa1115910.bv.player.entity.PlayerDefaultStartPosition\nimport dev.aaa1115910.bv.player.entity.NextVideoStrategy\nimport dev.aaa1115910.bv.player.entity.NextVideoStrategyConfig\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.player.entity.ControllerButtonConfig\nimport dev.aaa1115910.bv.player.entity.DefaultSubtitle\nimport dev.aaa1115910.bv.player.entity.LiveCodec\nimport dev.aaa1115910.bv.player.entity.getControllerButtonConfigsForEditing\nimport dev.aaa1115910.bv.player.entity.getControllerButtonDisplayName\nimport dev.aaa1115910.bv.player.entity.serializeControllerButtonsOrder\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.settings.SettingListItem\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.tv.component.settings.SettingListItemWithDialog\nimport dev.aaa1115910.bv.tv.component.settings.SettingSwitchListItem\nimport dev.aaa1115910.bv.tv.component.settings.SettingNumberListItem\nimport dev.aaa1115910.bv.tv.screens.settings.SettingsMenuNavItem\nimport dev.aaa1115910.bv.util.Prefs\n\n@Composable\nfun PlayerSetting(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n\n    var selectedResolution by remember { mutableStateOf(Prefs.defaultQuality) }\n    var selectedVideoCodec by remember { mutableStateOf(Prefs.defaultVideoCodec) }\n    var selectedAudio by remember { mutableStateOf(Prefs.defaultAudio) }\n    var enableFfmpegAudioRenderer by remember { mutableStateOf(Prefs.enableFfmpegAudioRenderer) }\n    var playerShowBottomProgressBar by remember { mutableStateOf(Prefs.playerShowBottomProgressBar) }\n    var playerShowDebugInfo by remember { mutableStateOf(Prefs.playerShowDebugInfo) }\n    var playerExitWhenAllIsPlayed by remember { mutableStateOf(Prefs.playerExitWhenAllIsPlayed) }\n    var showNextVideoStrategyDialog by remember { mutableStateOf(false) }\n    var playerDefaultStartPosition by remember { mutableStateOf(Prefs.playerDefaultStartPosition) }\n    var defaultPlaybackSpeed by remember { mutableDoubleStateOf(Prefs.defaultPlaySpeed.toDouble()) }\n    var playerSeekForwardStep by remember { mutableDoubleStateOf(Prefs.playerSeekForwardStep.toDouble()) }\n    var playerSeekBackwardStep by remember { mutableDoubleStateOf(Prefs.playerSeekBackwardStep.toDouble()) }\n    var portraitVideoFixMode by remember { mutableStateOf(Prefs.portraitVideoFixMode) }\n    var showOnlineViewerCountDialog by remember { mutableStateOf(false) }\n    val showOnlineViewerCount by Prefs.showOnlineViewerCountFlow.collectAsState(Prefs.showOnlineViewerCount)\n    var showLiveViewerCountTipDialog by remember { mutableStateOf(false) }\n    val showLiveViewerCountTip by Prefs.showLiveViewerCountTipFlow.collectAsState(Prefs.showLiveViewerCountTip)\n    var enableAsyncQueueing by remember { mutableStateOf(Prefs.enableAsyncQueueing) }\n    var skipPgcIntroOutro by remember { mutableStateOf(Prefs.skipPgcIntroOutro) }\n    var showControllerButtonDialog by remember { mutableStateOf(false) }\n    var defaultSubtitle by remember { mutableStateOf(Prefs.defaultSubtitle) }\n    var defaultLiveCodec by remember { mutableStateOf(Prefs.defaultLiveCodec) }\n    var defaultDanmakuFilterLevel by remember { mutableStateOf(Prefs.defaultDanmakuFilterLevel) }\n    var defaultLiveDanmakuFilterLevel by remember { mutableStateOf(Prefs.defaultLiveDanmakuFilterLevel) }\n    var showLongPressActionDialog by remember { mutableStateOf(false) }\n    var playerLongPressAction by remember { mutableIntStateOf(Prefs.playerLongPressAction) }\n    var playerLongPressSpeed by remember { mutableDoubleStateOf(Prefs.playerLongPressSpeed.toDouble()) }\n\n\n    Column(\n        modifier = modifier.fillMaxSize(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(12.dp)\n    ) {\n        Text(\n            text = SettingsMenuNavItem.Player.getDisplayName(context),\n            style = MaterialTheme.typography.displaySmall\n        )\n        Spacer(modifier = Modifier.height(12.dp))\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(horizontal = 48.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            item {\n                SettingListItemWithDialog(\n                    title = stringResource(R.string.settings_item_resolution),\n                    supportText = stringResource(R.string.settings_item_resolution),\n                    options = Resolution.entries.reversed(),\n                    getDisplayName = { item, ctx -> item.getDisplayName(ctx) },\n                    value = selectedResolution,\n                    onValueChange = {\n                        Prefs.defaultQuality = it\n                        selectedResolution = it\n                    }\n                )\n            }\n            item {\n                SettingListItemWithDialog(\n                    title = stringResource(R.string.settings_item_codec),\n                    supportText = stringResource(R.string.settings_item_codec),\n                    options = VideoCodec.entries.filter { it != VideoCodec.DVH1 && it != VideoCodec.HVC1 },\n                    getDisplayName = { item, ctx -> item.getDisplayName(ctx) },\n                    value = selectedVideoCodec,\n                    onValueChange = {\n                        Prefs.defaultVideoCodec = it\n                        selectedVideoCodec = it\n                    }\n                )\n            }\n            item {\n                SettingListItemWithDialog(\n                    title = stringResource(R.string.settings_item_audio),\n                    supportText = stringResource(R.string.settings_item_codec),\n                    options = Audio.entries,\n                    getDisplayName = { item, ctx -> item.getDisplayName(ctx) },\n                    value = selectedAudio,\n                    onValueChange = {\n                        Prefs.defaultAudio = it\n                        selectedAudio = it\n                    }\n                )\n            }\n            item {\n                SettingListItemWithDialog(\n                    title = stringResource(R.string.settings_item_live_codec),\n                    supportText = stringResource(R.string.settings_item_live_codec),\n                    options = LiveCodec.entries.toList(),\n                    getDisplayName = { item, ctx -> item.getDisplayName(ctx) },\n                    value = defaultLiveCodec,\n                    onValueChange = {\n                        defaultLiveCodec = it\n                        Prefs.defaultLiveCodec = it\n                    }\n                )\n            }\n            item {\n                SettingSwitchListItem(\n                    title = stringResource(R.string.settings_other_ffmpeg_audio_renderer_title),\n                    supportText = stringResource(R.string.settings_other_ffmpeg_audio_renderer_text),\n                    checked = enableFfmpegAudioRenderer,\n                    onCheckedChange = {\n                        enableFfmpegAudioRenderer = it\n                        Prefs.enableFfmpegAudioRenderer = it\n                    }\n                )\n            }\n            item {\n                SettingSwitchListItem(\n                    title = \"启用异步缓冲队列\",\n                    supportText = \"减少丢帧和音频欠载，提升高帧率视频播放性能（Android 6.0-11 有效）\",\n                    checked = enableAsyncQueueing,\n                    onCheckedChange = {\n                        enableAsyncQueueing = it\n                        Prefs.enableAsyncQueueing = it\n                    }\n                )\n            }\n            item {\n                SettingListItemWithDialog(\n                    title = stringResource(R.string.settings_portrait_video_fix_mode_title),\n                    supportText = stringResource(R.string.settings_portrait_video_fix_mode_text),\n                    options = PortraitVideoFixMode.entries,\n                    getDisplayName = { item, ctx -> item.displayName(ctx) },\n                    value = portraitVideoFixMode,\n                    onValueChange = {\n                        portraitVideoFixMode = it\n                        Prefs.portraitVideoFixMode = it\n                    }\n                )\n            }\n            item {\n                SettingListItem(\n                    title = stringResource(R.string.settings_player_load_next_action_title),\n                    supportText = stringResource(R.string.settings_player_load_next_action_text),\n                    onClick = { showNextVideoStrategyDialog = true }\n                )\n            }\n            item {\n                SettingSwitchListItem(\n                    title = stringResource(R.string.settings_player_exit_when_all_is_played_title),\n                    supportText = stringResource(R.string.settings_player_exit_when_all_is_played_text),\n                    checked = playerExitWhenAllIsPlayed,\n                    onCheckedChange = {\n                        playerExitWhenAllIsPlayed = it\n                        Prefs.playerExitWhenAllIsPlayed = it\n                    }\n                )\n            }\n            item {\n                SettingListItem(\n                    title = \"长按确认键行为\",\n                    supportText = \"设置播放器中长按确认键的行为\",\n                    valueText = when (playerLongPressAction) {\n                        0 -> \"打开菜单\"\n                        1 -> \"加速播放\"\n                        else -> \"打开菜单\"\n                    },\n                    onClick = { showLongPressActionDialog = true }\n                )\n            }\n            if (playerLongPressAction == 1) {\n                item {\n                    SettingNumberListItem(\n                        title = \"长按加速速度\",\n                        supportText = \"长按确认键时的播放速度\",\n                        value = playerLongPressSpeed,\n                        minValue = 1.25,\n                        maxValue = 3.0,\n                        isInteger = false,\n                        step = 0.25,\n                        onValueChange = {\n                            playerLongPressSpeed = it\n                            Prefs.playerLongPressSpeed = it.toFloat()\n                        }\n                    )\n                }\n            }\n            item {\n                SettingListItemWithDialog(\n                    title = stringResource(R.string.settings_player_default_start_position_title),\n                    supportText = stringResource(R.string.settings_player_default_start_position_text),\n                    options = PlayerDefaultStartPosition.entries,\n                    getDisplayName = { item, ctx -> item.displayName(ctx) },\n                    value = playerDefaultStartPosition,\n                    onValueChange = {\n                        playerDefaultStartPosition = it\n                        Prefs.playerDefaultStartPosition = it\n                    }\n                )\n            }\n            item {\n                SettingSwitchListItem(\n                    title = \"跳过 PGC 片头片尾\",\n                    supportText = \"自动跳过 PGC 片头片尾\",\n                    checked = skipPgcIntroOutro,\n                    onCheckedChange = {\n                        skipPgcIntroOutro = it\n                        Prefs.skipPgcIntroOutro = it\n                    }\n                )\n            }\n            item {\n                SettingListItemWithDialog(\n                    title = \"默认字幕\",\n                    supportText = \"首次播放时自动加载的字幕语言，仅加载非AI生成的字幕\",\n                    options = DefaultSubtitle.entries,\n                    getDisplayName = { item, _ -> item.displayName() },\n                    value = defaultSubtitle,\n                    onValueChange = {\n                        defaultSubtitle = it\n                        Prefs.defaultSubtitle = it\n                    }\n                )\n            }\n            item {\n                SettingNumberListItem(\n                    title = stringResource(R.string.settings_player_default_playback_speed_title),\n                    supportText = stringResource(R.string.settings_player_default_playback_speed_text),\n                    value = defaultPlaybackSpeed,\n                    minValue = 0.25,\n                    maxValue = 2.5,\n                    isInteger = false,\n                    step = 0.25,\n                    onValueChange = {\n                        defaultPlaybackSpeed = it\n                        Prefs.defaultPlaySpeed = it.toFloat()\n                    }\n                )\n            }\n            item {\n                SettingNumberListItem(\n                    title = stringResource(R.string.settings_player_seek_forward_step_title),\n                    supportText = stringResource(R.string.settings_player_seek_forward_step_text),\n                    value = playerSeekForwardStep,\n                    minValue = 5.0,\n                    maxValue = 30.0,\n                    isInteger = true,\n                    step = 1.0,\n                    onValueChange = {\n                        playerSeekForwardStep = it\n                        Prefs.playerSeekForwardStep = it.toInt()\n                    }\n                )\n            }\n            item {\n                SettingNumberListItem(\n                    title = stringResource(R.string.settings_player_seek_backward_step_title),\n                    supportText = stringResource(R.string.settings_player_seek_backward_step_text),\n                    value = playerSeekBackwardStep,\n                    minValue = 5.0,\n                    maxValue = 30.0,\n                    isInteger = true,\n                    step = 1.0,\n                    onValueChange = {\n                        playerSeekBackwardStep = it\n                        Prefs.playerSeekBackwardStep = it.toInt()\n                    }\n                )\n            }\n            item {\n                SettingNumberListItem(\n                    title = stringResource(R.string.settings_player_danmaku_filter_level_title),\n                    supportText = stringResource(R.string.settings_player_danmaku_filter_level_text),\n                    value = defaultDanmakuFilterLevel.toDouble(),\n                    minValue = 0.0,\n                    maxValue = 10.0,\n                    isInteger = true,\n                    step = 1.0,\n                    onValueChange = {\n                        defaultDanmakuFilterLevel = it.toInt()\n                        Prefs.defaultDanmakuFilterLevel = it.toInt()\n                    }\n                )\n            }\n            item {\n                SettingNumberListItem(\n                    title = stringResource(R.string.settings_live_danmaku_filter_level_title),\n                    supportText = stringResource(R.string.settings_live_danmaku_filter_level_text),\n                    value = defaultLiveDanmakuFilterLevel.toDouble(),\n                    minValue = 0.0,\n                    maxValue = 60.0,\n                    isInteger = true,\n                    step = 1.0,\n                    onValueChange = {\n                        defaultLiveDanmakuFilterLevel = it.toInt()\n                        Prefs.defaultLiveDanmakuFilterLevel = it.toInt()\n                    }\n                )\n            }\n            item {\n                SettingSwitchListItem(\n                    title = stringResource(R.string.settings_player_show_debug_info_title),\n                    supportText = stringResource(R.string.settings_player_show_debug_info_text),\n                    checked = playerShowDebugInfo,\n                    onCheckedChange = {\n                        playerShowDebugInfo = it\n                        Prefs.playerShowDebugInfo = it\n                    }\n                )\n            }\n            item {\n                SettingListItem(\n                    title = \"视频在线观看人数\",\n                    supportText = \"设置播放器在线人数显示方式\",\n                    valueText = when (showOnlineViewerCount) {\n                        0 -> \"不显示\"\n                        1 -> \"30 秒后隐藏\"\n                        2 -> \"始终显示\"\n                        else -> \"30 秒后隐藏\"\n                    },\n                    onClick = { showOnlineViewerCountDialog = true }\n                )\n            }\n            item {\n                SettingListItem(\n                    title = \"直播人气&高能观众\",\n                    supportText = \"设置直播人气和高能观众显示方式\",\n                    valueText = when (showLiveViewerCountTip) {\n                        0 -> \"不显示\"\n                        1 -> \"30 秒后隐藏\"\n                        2 -> \"始终显示\"\n                        else -> \"30 秒后隐藏\"\n                    },\n                    onClick = { showLiveViewerCountTipDialog = true }\n                )\n            }\n            item {\n                SettingListItem(\n                    title = \"播放器控制栏按钮\",\n                    supportText = \"自定义控制栏按钮的显示、排序和默认焦点\",\n                    onClick = { showControllerButtonDialog = true }\n                )\n            }\n            item {\n                SettingSwitchListItem(\n                    title = stringResource(R.string.settings_player_show_bottom_progress_bar_title),\n                    supportText = stringResource(R.string.settings_player_show_bottom_progress_bar_text),\n                    checked = playerShowBottomProgressBar,\n                    onCheckedChange = {\n                        playerShowBottomProgressBar = it\n                        Prefs.playerShowBottomProgressBar = it\n                    }\n                )\n            }\n        }\n\n        OnlineViewerCountDialog(\n            show = showOnlineViewerCountDialog,\n            onHideDialog = { showOnlineViewerCountDialog = false },\n            showOnlineViewerCount = showOnlineViewerCount,\n            onShowOnlineViewerCountChange = { Prefs.showOnlineViewerCount = it }\n        )\n\n        LiveViewerCountTipDialog(\n            show = showLiveViewerCountTipDialog,\n            onHideDialog = { showLiveViewerCountTipDialog = false },\n            showLiveViewerCountTip = showLiveViewerCountTip,\n            onShowLiveViewerCountTipChange = { Prefs.showLiveViewerCountTip = it }\n        )\n\n        PlayerControllerButtonDialog(\n            show = showControllerButtonDialog,\n            onHideDialog = { showControllerButtonDialog = false },\n            initialOrderString = Prefs.playerControllerButtonsOrder\n        )\n\n        NextVideoStrategyEditDialog(\n            show = showNextVideoStrategyDialog,\n            onHideDialog = { showNextVideoStrategyDialog = false }\n        )\n\n        LongPressActionDialog(\n            show = showLongPressActionDialog,\n            onHideDialog = { showLongPressActionDialog = false },\n            longPressAction = playerLongPressAction,\n            onLongPressActionChange = {\n                playerLongPressAction = it\n                Prefs.playerLongPressAction = it\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun OnlineViewerCountDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    showOnlineViewerCount: Int,\n    onShowOnlineViewerCountChange: (Int) -> Unit\n) {\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = { onHideDialog() },\n            title = { Text(text = \"视频在线观看人数\") },\n            text = {\n                Column {\n                    val options = listOf(\n                        \"不显示\" to 0,\n                        \"30 秒后隐藏\" to 1,\n                        \"始终显示\" to 2\n                    )\n                    options.forEach { (text, value) ->\n                        ListItem(\n                            selected = showOnlineViewerCount == value,\n                            onClick = { onShowOnlineViewerCountChange(value) },\n                            headlineContent = { Text(text = text) },\n                            trailingContent = {\n                                RadioButton(\n                                    selected = showOnlineViewerCount == value,\n                                    onClick = null\n                                )\n                            }\n                        )\n                    }\n                }\n            },\n            confirmButton = {}\n        )\n    }\n}\n\n@Composable\nprivate fun LiveViewerCountTipDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    showLiveViewerCountTip: Int,\n    onShowLiveViewerCountTipChange: (Int) -> Unit\n) {\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = { onHideDialog() },\n            title = { Text(text = \"直播人气&高能观众\") },\n            text = {\n                Column {\n                    val options = listOf(\n                        \"不显示\" to 0,\n                        \"30 秒后隐藏\" to 1,\n                        \"始终显示\" to 2\n                    )\n                    options.forEach { (text, value) ->\n                        ListItem(\n                            selected = showLiveViewerCountTip == value,\n                            onClick = { onShowLiveViewerCountTipChange(value) },\n                            headlineContent = { Text(text = text) },\n                            trailingContent = {\n                                RadioButton(\n                                    selected = showLiveViewerCountTip == value,\n                                    onClick = null\n                                )\n                            }\n                        )\n                    }\n                }\n            },\n            confirmButton = {}\n        )\n    }\n}\n\n@Composable\nprivate fun LongPressActionDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    longPressAction: Int,\n    onLongPressActionChange: (Int) -> Unit\n) {\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = { onHideDialog() },\n            title = { Text(text = \"长按确认键行为\") },\n            text = {\n                Column {\n                    val options = listOf(\n                        \"打开菜单\" to 0,\n                        \"加速播放\" to 1\n                    )\n                    options.forEach { (text, value) ->\n                        ListItem(\n                            selected = longPressAction == value,\n                            onClick = { onLongPressActionChange(value) },\n                            headlineContent = { Text(text = text) },\n                            trailingContent = {\n                                RadioButton(\n                                    selected = longPressAction == value,\n                                    onClick = null\n                                )\n                            }\n                        )\n                    }\n                }\n            },\n            confirmButton = {}\n        )\n    }\n}\n\n@Composable\nprivate fun PlayerControllerButtonDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    initialOrderString: String\n) {\n    if (!show) return\n\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n    val listState = rememberLazyListState()\n\n    val initialConfigs = remember(initialOrderString) {\n        getControllerButtonConfigsForEditing(initialOrderString)\n    }\n\n    var buttonConfigs by remember { mutableStateOf(initialConfigs) }\n    var selectedIndex by remember { mutableIntStateOf(0) }\n    var enterDownTime by remember { mutableStateOf(0L) }\n    var longPressHandled by remember { mutableStateOf(false) }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus(scope)\n    }\n\n    // 自动滚动到选中项\n    LaunchedEffect(selectedIndex) {\n        listState.animateScrollToItem(\n            index = (selectedIndex - 2).coerceAtLeast(0)\n        )\n    }\n\n    TvAlertDialog(\n        modifier = modifier,\n        onDismissRequest = {\n            Prefs.playerControllerButtonsOrder = serializeControllerButtonsOrder(buttonConfigs)\n            onHideDialog()\n        },\n        title = { Text(text = \"控制栏按钮\") },\n        text = {\n            Column(\n                modifier = Modifier\n                    .focusRequester(focusRequester)\n                    .focusable()\n                    .onPreviewKeyEvent {\n                        if (it.type == KeyEventType.KeyDown) {\n                            when (it.key) {\n                                Key.DirectionLeft -> {\n                                    if (selectedIndex > 0) {\n                                        buttonConfigs = buttonConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex - 1]\n                                            this[selectedIndex - 1] = temp\n                                        }\n                                        selectedIndex--\n                                    }\n                                    true\n                                }\n                                Key.DirectionRight -> {\n                                    if (selectedIndex < buttonConfigs.size - 1) {\n                                        buttonConfigs = buttonConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex + 1]\n                                            this[selectedIndex + 1] = temp\n                                        }\n                                        selectedIndex++\n                                    }\n                                    true\n                                }\n                                Key.DirectionUp -> {\n                                    if (selectedIndex > 0) selectedIndex--\n                                    true\n                                }\n                                Key.DirectionDown -> {\n                                    if (selectedIndex < buttonConfigs.size - 1) selectedIndex++\n                                    true\n                                }\n                                Key.Enter, Key.DirectionCenter -> {\n                                    if (enterDownTime == 0L) {\n                                        // 首次按下，记录时间\n                                        enterDownTime = System.currentTimeMillis()\n                                        longPressHandled = false\n                                    } else if (!longPressHandled && System.currentTimeMillis() - enterDownTime >= 600) {\n                                        // 在重复 KeyDown 期间检测到长按\n                                        longPressHandled = true\n                                        buttonConfigs = buttonConfigs.toMutableList().apply {\n                                            for (i in indices) {\n                                                this[i] = this[i].copy(\n                                                    isDefaultFocus = i == selectedIndex\n                                                )\n                                            }\n                                        }\n                                    }\n                                    true\n                                }\n                                else -> false\n                            }\n                        } else if (it.type == KeyEventType.KeyUp) {\n                            when (it.key) {\n                                Key.Enter, Key.DirectionCenter -> {\n                                    if (!longPressHandled && enterDownTime > 0L) {\n                                        // 短按：切换显示/隐藏\n                                        val config = buttonConfigs[selectedIndex]\n                                        buttonConfigs = buttonConfigs.toMutableList().apply {\n                                            this[selectedIndex] = config.copy(hidden = !config.hidden)\n                                        }\n                                    }\n                                    enterDownTime = 0L\n                                    longPressHandled = false\n                                    true\n                                }\n                                else -> false\n                            }\n                        } else false\n                    }\n            ) {\n                Text(\n                    text = \"左右键排序 · 短按确认键显示/隐藏 · 长按确认键设为默认焦点\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n                Spacer(modifier = Modifier.height(8.dp))\n\n                LazyColumn(\n                    modifier = Modifier.fillMaxWidth(),\n                    state = listState,\n                    verticalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    itemsIndexed(\n                        items = buttonConfigs,\n                        key = { index, config -> \"$index-button-${config.id}\" }\n                    ) { index, config ->\n                        ControllerButtonEditRow(\n                            title = getControllerButtonDisplayName(config.id),\n                            hidden = config.hidden,\n                            isDefaultFocus = config.isDefaultFocus,\n                            selected = index == selectedIndex\n                        )\n                    }\n                }\n            }\n        },\n        confirmButton = {}\n    )\n}\n\n@Composable\nprivate fun ControllerButtonEditRow(\n    title: String,\n    hidden: Boolean,\n    isDefaultFocus: Boolean,\n    selected: Boolean\n) {\n    val shape = remember { RoundedCornerShape(12.dp) }\n    val bgColor = if (selected) MaterialTheme.colorScheme.onBackground else Color.Transparent\n\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .background(color = bgColor, shape = shape)\n            .padding(horizontal = 20.dp, vertical = 12.dp),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.SpaceBetween\n    ) {\n        Text(\n            text = title,\n            textDecoration = if (hidden) TextDecoration.LineThrough else null,\n            color = if (selected) {\n                MaterialTheme.colorScheme.background\n            } else if (hidden) {\n                MaterialTheme.colorScheme.onSurfaceVariant\n            } else {\n                MaterialTheme.colorScheme.onSurface\n            }\n        )\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            if (isDefaultFocus) {\n                Text(\n                    text = \"默认焦点\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = if (selected) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.primary\n                )\n            }\n            if (hidden) {\n                Text(\n                    text = \"隐藏\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = if (selected) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.error\n                )\n            } else {\n                Text(\n                    text = \"显示\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = if (selected) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.onSurfaceVariant\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun NextVideoStrategyEditDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit\n) {\n    if (!show) return\n\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    var strategyConfigs by remember {\n        mutableStateOf(\n            run {\n                val validOrdinals = NextVideoStrategy.entries.map { it.ordinalValue }.toSet()\n                val parsedConfigs = Prefs.playerNextVideoStrategyOrder.split(\",\").mapNotNull {\n                    if (it.isBlank()) return@mapNotNull null\n                    val hidden = it.startsWith(\"-\")\n                    val id = it.replace(\"-\", \"\").toIntOrNull() ?: return@mapNotNull null\n                    if (id !in validOrdinals) return@mapNotNull null\n                    val strategy = NextVideoStrategy.fromOrdinal(id)\n                    if (strategy == NextVideoStrategy.SingleVideo) return@mapNotNull null\n                    NextVideoStrategyConfig(strategy, hidden, id)\n                }.toMutableList()\n                val allStrategies = NextVideoStrategy.entries.filter { it != NextVideoStrategy.SingleVideo }\n                val existingIds = parsedConfigs.map { it.ordinal }.toSet()\n                // 互斥对：启用其中一个时，另一个默认禁用\n                val mutualExclusivePairs = mapOf(\n                    NextVideoStrategy.PreloadedVideoList.ordinalValue to NextVideoStrategy.PreloadedVideoListReverse.ordinalValue,\n                    NextVideoStrategy.PreloadedVideoListReverse.ordinalValue to NextVideoStrategy.PreloadedVideoList.ordinalValue,\n                    NextVideoStrategy.PartAndEpisode.ordinalValue to NextVideoStrategy.PartAndEpisodeReverse.ordinalValue,\n                    NextVideoStrategy.PartAndEpisodeReverse.ordinalValue to NextVideoStrategy.PartAndEpisode.ordinalValue,\n                )\n                val enabledIds = parsedConfigs.filter { !it.hidden }.map { it.ordinal }.toSet()\n                allStrategies.forEach { strategy ->\n                    if (strategy.ordinalValue !in existingIds) {\n                        // 如果互斥对中的另一方已启用，则默认禁用\n                        val pairOrdinal = mutualExclusivePairs[strategy.ordinalValue]\n                        val shouldHide = pairOrdinal != null && pairOrdinal in enabledIds\n                        parsedConfigs.add(NextVideoStrategyConfig(strategy, hidden = shouldHide, ordinal = strategy.ordinalValue))\n                    }\n                }\n                parsedConfigs.toList()\n            }\n        )\n    }\n    var selectedIndex by remember { mutableIntStateOf(0) }\n    val listState = rememberLazyListState()\n\n    LaunchedEffect(selectedIndex, strategyConfigs.size) {\n        if (strategyConfigs.isEmpty()) return@LaunchedEffect\n\n        val layoutInfo = listState.layoutInfo\n        val viewportStart = layoutInfo.viewportStartOffset\n        val viewportEnd = layoutInfo.viewportEndOffset\n        val viewportSize = viewportEnd - viewportStart\n        if (viewportSize <= 0) return@LaunchedEffect\n\n        val visibleItems = layoutInfo.visibleItemsInfo\n        val visibleCount = visibleItems.size\n        if (visibleCount <= 0) return@LaunchedEffect\n\n        val firstVisible = listState.firstVisibleItemIndex\n        val selectedItemInfo = visibleItems.firstOrNull { it.index == selectedIndex }\n\n        if (selectedItemInfo != null) {\n            val itemStart = selectedItemInfo.offset\n            val itemEnd = itemStart + selectedItemInfo.size\n\n            if (itemStart < viewportStart) {\n                listState.animateScrollToItem(index = selectedIndex, scrollOffset = 0)\n                return@LaunchedEffect\n            }\n\n            if (itemEnd > viewportEnd) {\n                val bottomAlignedOffset = (viewportSize - selectedItemInfo.size).coerceAtLeast(0)\n                listState.animateScrollToItem(index = selectedIndex, scrollOffset = bottomAlignedOffset)\n                return@LaunchedEffect\n            }\n\n            val middleIndex = firstVisible + visibleCount / 2\n            if (selectedIndex > middleIndex) {\n                val maxFirstVisible = (strategyConfigs.size - visibleCount).coerceAtLeast(0)\n                val targetFirstVisible = (selectedIndex - visibleCount / 2)\n                    .coerceIn(0, maxFirstVisible)\n                if (targetFirstVisible != firstVisible) {\n                    listState.animateScrollToItem(index = targetFirstVisible)\n                }\n            }\n            return@LaunchedEffect\n        }\n\n        val maxFirstVisible = (strategyConfigs.size - visibleCount).coerceAtLeast(0)\n        val targetFirstVisible = (selectedIndex - visibleCount / 2)\n            .coerceIn(0, maxFirstVisible)\n        if (targetFirstVisible != firstVisible) {\n            listState.animateScrollToItem(index = targetFirstVisible)\n        }\n    }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus(scope)\n    }\n\n    TvAlertDialog(\n        modifier = modifier,\n        onDismissRequest = {\n            Prefs.playerNextVideoStrategyOrder = strategyConfigs.joinToString(\",\") { config ->\n                if (config.hidden) \"-${config.ordinal}\" else \"${config.ordinal}\"\n            }\n            onHideDialog()\n        },\n        title = { Text(text = stringResource(R.string.settings_player_load_next_action_title)) },\n        text = {\n            Column(\n                modifier = Modifier\n                    .focusRequester(focusRequester)\n                    .focusable()\n                    .onPreviewKeyEvent {\n                        if (it.type == KeyEventType.KeyDown) {\n                            when (it.key) {\n                                Key.DirectionLeft -> {\n                                    if (selectedIndex > 0) {\n                                        strategyConfigs = strategyConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex - 1]\n                                            this[selectedIndex - 1] = temp\n                                        }\n                                        selectedIndex--\n                                    }\n                                    true\n                                }\n                                Key.DirectionRight -> {\n                                    if (selectedIndex < strategyConfigs.size - 1) {\n                                        strategyConfigs = strategyConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex + 1]\n                                            this[selectedIndex + 1] = temp\n                                        }\n                                        selectedIndex++\n                                    }\n                                    true\n                                }\n                                Key.DirectionUp -> {\n                                    if (selectedIndex > 0) selectedIndex--\n                                    true\n                                }\n                                Key.DirectionDown -> {\n                                    if (selectedIndex < strategyConfigs.size - 1) selectedIndex++\n                                    true\n                                }\n                                Key.Enter, Key.DirectionCenter -> {\n                                    val config = strategyConfigs[selectedIndex]\n                                    val newHidden = !config.hidden\n                                    strategyConfigs = strategyConfigs.toMutableList().apply {\n                                        this[selectedIndex] = config.copy(hidden = newHidden)\n                                        // 互斥处理：启用时自动禁用对应项\n                                        if (!newHidden) {\n                                            val mutualExclusiveOrdinal = when (config.strategy) {\n                                                NextVideoStrategy.PreloadedVideoList -> NextVideoStrategy.PreloadedVideoListReverse.ordinalValue\n                                                NextVideoStrategy.PreloadedVideoListReverse -> NextVideoStrategy.PreloadedVideoList.ordinalValue\n                                                NextVideoStrategy.PartAndEpisode -> NextVideoStrategy.PartAndEpisodeReverse.ordinalValue\n                                                NextVideoStrategy.PartAndEpisodeReverse -> NextVideoStrategy.PartAndEpisode.ordinalValue\n                                                else -> null\n                                            }\n                                            if (mutualExclusiveOrdinal != null) {\n                                                val pairIndex = indexOfFirst { it.ordinal == mutualExclusiveOrdinal }\n                                                if (pairIndex >= 0) {\n                                                    this[pairIndex] = this[pairIndex].copy(hidden = true)\n                                                }\n                                            }\n                                        }\n                                    }\n                                    true\n                                }\n                                else -> false\n                            }\n                        } else false\n                    }\n            ) {\n                Text(\n                    text = \"左右键排序 · 短按确认键禁用/启用\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n                Spacer(modifier = Modifier.height(8.dp))\n\n                LazyColumn(\n                    modifier = Modifier.fillMaxWidth(),\n                    state = listState,\n                    verticalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    itemsIndexed(\n                        items = strategyConfigs,\n                        key = { index, config -> \"${index}-strategy-${config.ordinal}\" }\n                    ) { index, config ->\n                        NextVideoStrategyEditRow(\n                            title = config.strategy.displayName(LocalContext.current),\n                            hidden = config.hidden,\n                            selected = index == selectedIndex\n                        )\n                    }\n                }\n            }\n        },\n        confirmButton = {}\n    )\n}\n\n@Composable\nprivate fun NextVideoStrategyEditRow(\n    title: String,\n    hidden: Boolean,\n    selected: Boolean\n) {\n    val shape = remember { RoundedCornerShape(12.dp) }\n    val bgColor = if (selected) MaterialTheme.colorScheme.onBackground else Color.Transparent\n\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .background(color = bgColor, shape = shape)\n            .padding(horizontal = 20.dp, vertical = 12.dp),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.SpaceBetween\n    ) {\n        Text(\n            text = title,\n            textDecoration = if (hidden) TextDecoration.LineThrough else null,\n            color = if (selected) {\n                MaterialTheme.colorScheme.background\n            } else if (hidden) {\n                MaterialTheme.colorScheme.onSurfaceVariant\n            } else {\n                MaterialTheme.colorScheme.onSurface\n            }\n        )\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            if (hidden) {\n                Text(\n                    text = \"禁用\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = if (selected) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.error\n                )\n            } else {\n                Text(\n                    text = \"启用\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = if (selected) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.onSurfaceVariant\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/content/PlayerTypeSetting.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings.content\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.tv.component.LibVLCDownloaderDialog\nimport dev.aaa1115910.bv.tv.component.settings.SettingsMenuSelectItem\nimport dev.aaa1115910.bv.entity.PlayerType\nimport dev.aaa1115910.bv.tv.screens.settings.SettingsMenuNavItem\nimport dev.aaa1115910.bv.util.Prefs\n\n@Composable\nfun PlayerTypeSetting(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n    var selectedPlayerType by remember { mutableStateOf(Prefs.playerType) }\n    var showLibVLCDownloaderDialog by remember { mutableStateOf(false) }\n\n    Box(\n        modifier = modifier\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(horizontal = 48.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            Text(\n                text = SettingsMenuNavItem.PlayerType.getDisplayName(context),\n                style = MaterialTheme.typography.displaySmall\n            )\n            Spacer(modifier = Modifier.height(12.dp))\n            LazyColumn(\n                verticalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                itemsIndexed(\n                    items = PlayerType.entries,\n                    key = { index, playerType -> \"$index-player-${playerType.name}\" }\n                ) { _, playerType ->\n                    SettingsMenuSelectItem(\n                        text = playerType.name,\n                        selected = selectedPlayerType == playerType,\n                        onClick = {\n                            selectedPlayerType = playerType\n                            Prefs.playerType = playerType\n                        }\n                    )\n                }\n            }\n        }\n    }\n\n    LibVLCDownloaderDialog(\n        show = showLibVLCDownloaderDialog,\n        onHideDialog = {\n            showLibVLCDownloaderDialog = false\n        }\n    )\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/content/StorageSetting.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings.content\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.settings.SettingListItem\nimport dev.aaa1115910.bv.tv.screens.settings.SettingsMenuNavItem\nimport dev.aaa1115910.bv.util.LogCatcherUtil\nimport dev.aaa1115910.bv.util.fInfo\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.File\n\n@Composable\nfun StorageSetting(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger { }\n\n    var loading by remember { mutableStateOf(false) }\n    var imageCacheSize by remember { mutableLongStateOf(0L) }\n    var updateCacheSize by remember { mutableLongStateOf(0L) }\n    var crashLogsSize by remember { mutableLongStateOf(0L) }\n    //var libVLCCacheSize by remember { mutableLongStateOf(0L) }\n    //var libVLCFileSize by remember { mutableLongStateOf(0L) }\n\n    var showConfirmDialog by remember { mutableStateOf(false) }\n    var clearFun: (() -> Unit)? by remember { mutableStateOf(null) }\n    var content by remember { mutableStateOf(\"\") }\n    var size by remember { mutableLongStateOf(0L) }\n\n    val calSize = {\n        scope.launch(Dispatchers.IO) {\n            val imageCacheDir = File(context.cacheDir, \"image_cache\")\n            val updateCacheDir = File(context.cacheDir, \"update_downloader\")\n            val crashLogsDir = File(context.filesDir, LogCatcherUtil.LOG_DIR)\n            //val libVLCCacheDir = File(context.cacheDir, \"libvlc_downloader\")\n            //val libVLCFileDir = File(context.filesDir, \"vlc_libs\")\n\n            val newImageCacheSize = getFolderSize(imageCacheDir)\n            val newUpdateCacheSize = getFolderSize(updateCacheDir)\n            val newCrashLogsSize = getFolderSize(crashLogsDir)\n            //val newLibVLCCacheSize = getFolderSize(libVLCCacheDir)\n            //val newLibVLCFileSize = getFolderSize(libVLCFileDir)\n\n            withContext(Dispatchers.Main) {\n                imageCacheSize = newImageCacheSize\n                updateCacheSize = newUpdateCacheSize\n                crashLogsSize = newCrashLogsSize\n                //libVLCCacheSize = newLibVLCCacheSize\n                //libVLCFileSize = newLibVLCFileSize\n            }\n        }\n    }\n\n    val clearImageCaches: () -> Unit = {\n        logger.fInfo { \"clearImageCaches\" }\n        val imageCacheDir = File(context.cacheDir, \"image_cache\")\n        imageCacheDir.deleteRecursively()\n    }\n\n    val clearCrashLogs: () -> Unit = {\n        logger.fInfo { \"clearCrashLogs\" }\n        val crashLogsDir = File(context.filesDir, LogCatcherUtil.LOG_DIR)\n        crashLogsDir.deleteRecursively()\n    }\n\n    val clearOthersCaches: () -> Unit = {\n        logger.fInfo { \"clearOthersCaches\" }\n        val updateCacheDir = File(context.cacheDir, \"update_downloader\")\n        //val libVLCCacheDir = File(context.cacheDir, \"libvlc_downloader\")\n        updateCacheDir.deleteRecursively()\n        //libVLCCacheDir.deleteRecursively()\n    }\n\n    //val clearLibVLCFiles: () -> Unit = {\n    //    logger.fInfo { \"clearLibVLCFiles\" }\n    //    val libVLCFileDir = File(context.filesDir, \"vlc_libs\")\n    //    libVLCFileDir.deleteRecursively()\n    //}\n\n    LaunchedEffect(Unit) {\n        loading = true\n        calSize()\n        loading = false\n    }\n\n    Box(modifier = modifier) {\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(horizontal = 48.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            Text(\n                text = SettingsMenuNavItem.Storage.getDisplayName(context),\n                style = MaterialTheme.typography.displaySmall\n            )\n            Spacer(modifier = Modifier.height(12.dp))\n            LazyColumn(\n                verticalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_storage_image_cache),\n                        supportText = if (loading) stringResource(R.string.settings_storage_calculating)\n                        else \"${imageCacheSize / 1024 / 1024} MB\",\n                        onClick = {\n                            clearFun = clearImageCaches\n                            content = context.getString(R.string.settings_storage_image_cache)\n                            size = imageCacheSize\n                            showConfirmDialog = true\n                        }\n                    )\n                }\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_storage_others_cache),\n                        supportText = if (loading) stringResource(R.string.settings_storage_calculating)\n                        //else \"${updateCacheSize + libVLCCacheSize / 1024 / 1024} MB\",\n                        else \"${updateCacheSize / 1024 / 1024} MB\",\n                        onClick = {\n                            clearFun = clearOthersCaches\n                            content = context.getString(R.string.settings_storage_others_cache)\n                            size = updateCacheSize// + libVLCCacheSize\n                            showConfirmDialog = true\n                        }\n                    )\n                }\n                //item {\n                //    SettingListItem(\n                //        title = stringResource(R.string.settings_storage_libvlc_files),\n                //        supportText = if (loading) stringResource(R.string.settings_storage_calculating)\n                //        else \"${libVLCFileSize / 1024 / 1024} MB\",\n                //        onClick = {\n                //            clearFun = clearLibVLCFiles\n                //            content = context.getString(R.string.settings_storage_libvlc_files)\n                //            size = libVLCFileSize\n                //            showConfirmDialog = true\n                //        }\n                //    )\n                //}\n\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_storage_crash_logs),\n                        supportText = if (loading) stringResource(R.string.settings_storage_calculating)\n                        else \"${crashLogsSize / 1024 / 1024} MB\",\n                        onClick = {\n                            clearFun = clearCrashLogs\n                            content = context.getString(R.string.settings_storage_crash_logs)\n                            size = crashLogsSize\n                            showConfirmDialog = true\n                        }\n                    )\n                }\n            }\n        }\n    }\n\n    ConfirmDeleteDialog(\n        show = showConfirmDialog,\n        onHideDialog = { showConfirmDialog = false },\n        content = content,\n        size = size,\n        clearFiles = {\n            clearFun?.invoke()\n            calSize()\n        }\n    )\n}\n\nprivate fun getFolderSize(f: File): Long {\n    var size: Long = 0\n    if (f.isDirectory) {\n        for (file in f.listFiles()!!) {\n            size += getFolderSize(file)\n        }\n    } else {\n        size = f.length()\n    }\n    return size\n}\n\n@Composable\nprivate fun ConfirmDeleteDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    content: String,\n    size: Long,\n    clearFiles: () -> Unit\n) {\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = onHideDialog,\n            title = { Text(text = \"清除$content\") },\n            text = { Text(text = \"${size / 1024 / 1024} MB\") },\n            confirmButton = {\n                Button(onClick = {\n                    clearFiles()\n                    onHideDialog()\n                }) {\n                    Text(text = \"确定\")\n                }\n            },\n            dismissButton = {\n                OutlinedButton(onClick = onHideDialog) {\n                    Text(text = \"取消\")\n                }\n            }\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/settings/content/UISetting.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.settings.content\n\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.ArrowDropDown\nimport androidx.compose.material.icons.rounded.ArrowDropUp\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.ListItem\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.RadioButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.activities.LauncherActivity\nimport dev.aaa1115910.bv.entity.InterfaceMode\nimport dev.aaa1115910.bv.entity.NavSwitchMode\nimport dev.aaa1115910.bv.entity.ThemeType\nimport dev.aaa1115910.bv.tv.component.PgcTopNavItem\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.UgcTopNavItem\nimport dev.aaa1115910.bv.tv.component.settings.SettingListItem\nimport dev.aaa1115910.bv.tv.component.settings.SettingNumberListItem\nimport dev.aaa1115910.bv.tv.component.settings.SettingSwitchListItem\nimport dev.aaa1115910.bv.tv.component.HomeTopNavItem\nimport dev.aaa1115910.bv.tv.screens.main.DrawerItem\nimport dev.aaa1115910.bv.tv.screens.settings.SettingsMenuNavItem\nimport dev.aaa1115910.bv.tv.util.NavItemConfig\nimport dev.aaa1115910.bv.tv.util.LiveNavItemConfig\nimport dev.aaa1115910.bv.tv.util.getLiveNavItemDisplayName\nimport dev.aaa1115910.bv.tv.util.parseCachedLiveAreaGroups\nimport dev.aaa1115910.bv.tv.util.parseDrawerNavItemsOrderToConfig\nimport dev.aaa1115910.bv.tv.util.parseLiveNavItemsOrderToConfig\nimport dev.aaa1115910.bv.tv.util.parseNavItemsOrderToConfig\nimport dev.aaa1115910.bv.tv.util.saveDrawerNavConfigs\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.requestFocus\nimport kotlin.math.roundToInt\n\n@Composable\nfun UISetting(\n    modifier: Modifier = Modifier\n) {\n    val context = LocalContext.current\n\n    var showDensityDialog by remember { mutableStateOf(false) }\n    var showThemeTypeDialog by remember { mutableStateOf(false) }\n    var showInterfaceModeDialog by remember { mutableStateOf(false) }\n    var showNavSwitchModeDialog by remember { mutableStateOf(false) }\n    var showHomeNavItemsDialog by remember { mutableStateOf(false) }\n    var showUgcNavItemsDialog by remember { mutableStateOf(false) }\n    var showPgcNavItemsDialog by remember { mutableStateOf(false) }\n    var showLiveNavItemsDialog by remember { mutableStateOf(false) }\n    var showDrawerNavItemsDialog by remember { mutableStateOf(false) }\n    val density by Prefs.densityFlow.collectAsState(context.resources.displayMetrics.widthPixels / 960f)\n    val themeType by Prefs.themeTypeFlow.collectAsState(Prefs.themeType)\n    val interfaceMode = Prefs.interfaceMode\n    val navSwitchMode by Prefs.navSwitchModeFlow.collectAsState(Prefs.navSwitchMode)\n    var showUGCVideoInfo by remember { mutableStateOf(Prefs.showUGCVideoInfo) }\n    var videoInfoHistoryIncludeFromPlayer by remember { mutableStateOf(Prefs.videoInfoHistoryIncludeFromPlayer) }\n    var ugcVideoInfoHistoryCount by remember { mutableIntStateOf(Prefs.ugcVideoInfoHistoryCount) }\n    var ugcVideoPlayerHistoryCount by remember { mutableIntStateOf(Prefs.ugcVideoPlayerHistoryCount) }\n\n    Box(modifier = modifier) {\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(horizontal = 48.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(12.dp)\n        ) {\n            Text(\n                text = SettingsMenuNavItem.UI.getDisplayName(context),\n                style = MaterialTheme.typography.displaySmall\n            )\n            Spacer(modifier = Modifier.height(12.dp))\n            LazyColumn(\n                verticalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_ui_interface_mode_title),\n                        supportText = stringResource(R.string.settings_ui_interface_mode_text),\n                        valueText = interfaceMode.getDisplayName(context),\n                        onClick = { showInterfaceModeDialog = true }\n                    )\n                }\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_ui_density_title),\n                        supportText = stringResource(R.string.settings_ui_density_text),\n                        valueText = density.toString(),\n                        onClick = { showDensityDialog = true }\n                    )\n                }\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_ui_theme_type_title),\n                        supportText = stringResource(R.string.settings_ui_theme_type_text),\n                        valueText = themeType.getDisplayName(context),\n                        onClick = { showThemeTypeDialog = true }\n                    )\n                }\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_ui_nav_switch_mode_title),\n                        supportText = stringResource(R.string.settings_ui_nav_switch_mode_text),\n                        valueText = navSwitchMode.getDisplayName(context),\n                        onClick = { showNavSwitchModeDialog = true }\n                    )\n                }\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_ui_drawer_nav_items_title),\n                        supportText = stringResource(R.string.settings_ui_drawer_nav_items_text),\n                        onClick = { showDrawerNavItemsDialog = true }\n                    )\n                }\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_ui_home_nav_items_title),\n                        supportText = stringResource(R.string.settings_ui_home_nav_items_text),\n                        onClick = { showHomeNavItemsDialog = true }\n                    )\n                }\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_ui_ugc_nav_items_title),\n                        supportText = stringResource(R.string.settings_ui_ugc_nav_items_text),\n                        onClick = { showUgcNavItemsDialog = true }\n                    )\n                }\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_ui_pgc_nav_items_title),\n                        supportText = stringResource(R.string.settings_ui_pgc_nav_items_text),\n                        onClick = { showPgcNavItemsDialog = true }\n                    )\n                }\n                item {\n                    SettingListItem(\n                        title = stringResource(R.string.settings_ui_live_nav_items_title),\n                        supportText = stringResource(R.string.settings_ui_live_nav_items_text),\n                        onClick = { showLiveNavItemsDialog = true }\n                    )\n                }\n                item {\n                    SettingSwitchListItem(\n                        title = stringResource(R.string.settings_show_ugc_video_info_title),\n                        supportText = stringResource(R.string.settings_show_ugc_video_info_text),\n                        checked = showUGCVideoInfo,\n                        onCheckedChange = {\n                            showUGCVideoInfo = it\n                            Prefs.showUGCVideoInfo = it\n                        }\n                    )\n                }\n                if (showUGCVideoInfo) {\n                    item {\n                        SettingNumberListItem(\n                            title = stringResource(R.string.settings_ui_ugc_video_info_history_count_title),\n                            supportText = stringResource(R.string.settings_ui_ugc_video_info_history_count_text),\n                            value = ugcVideoInfoHistoryCount.toDouble(),\n                            minValue = 1.0,\n                            maxValue = 10.0,\n                            isInteger = true,\n                            step = 1.0,\n                            onValueChange = {\n                                ugcVideoInfoHistoryCount = it.toInt()\n                                Prefs.ugcVideoInfoHistoryCount = it.toInt()\n                            }\n                        )\n                    }\n                    item {\n                        SettingSwitchListItem(\n                            title = stringResource(R.string.settings_ui_video_info_history_include_from_player_title),\n                            supportText = stringResource(R.string.settings_ui_video_info_history_include_from_player_text),\n                            checked = videoInfoHistoryIncludeFromPlayer,\n                            onCheckedChange = {\n                                videoInfoHistoryIncludeFromPlayer = it\n                                Prefs.videoInfoHistoryIncludeFromPlayer = it\n                            }\n                        )\n                    }\n                }\n                if (!showUGCVideoInfo) {\n                    item {\n                        SettingNumberListItem(\n                            title = stringResource(R.string.settings_ui_ugc_video_player_history_count_title),\n                            supportText = stringResource(R.string.settings_ui_ugc_video_player_history_count_text),\n                            value = ugcVideoPlayerHistoryCount.toDouble(),\n                            minValue = 1.0,\n                            maxValue = 10.0,\n                            isInteger = true,\n                            step = 1.0,\n                            onValueChange = {\n                                ugcVideoPlayerHistoryCount = it.toInt()\n                                Prefs.ugcVideoPlayerHistoryCount = it.toInt()\n                            }\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    UIDensityDialog(\n        show = showDensityDialog,\n        onHideDialog = { showDensityDialog = false },\n        density = density,\n        onDensityChange = { Prefs.density = it }\n    )\n\n    ThemeTypeDialog(\n        show = showThemeTypeDialog,\n        onHideDialog = { showThemeTypeDialog = false },\n        themeType = themeType,\n        onThemeTypeChange = { Prefs.themeType = it }\n    )\n\n    InterfaceModeDialog(\n        show = showInterfaceModeDialog,\n        onHideDialog = { showInterfaceModeDialog = false },\n        interfaceMode = interfaceMode,\n        onInterfaceModeChange = {\n            if (it != interfaceMode) {\n                Prefs.interfaceMode = it\n                LauncherActivity.actionRestart(context)\n            }\n        }\n    )\n\n    NavSwitchModeDialog(\n        show = showNavSwitchModeDialog,\n        onHideDialog = { showNavSwitchModeDialog = false },\n        navSwitchMode = navSwitchMode,\n        onNavSwitchModeChange = { Prefs.navSwitchMode = it }\n    )\n\n    HomeNavItemsEditDialog(\n        show = showHomeNavItemsDialog,\n        onHideDialog = { showHomeNavItemsDialog = false },\n        initialOrderString = Prefs.homeNavItemsOrder\n    )\n\n    UgcNavItemsEditDialog(\n        show = showUgcNavItemsDialog,\n        onHideDialog = { showUgcNavItemsDialog = false },\n        initialOrderString = Prefs.ugcNavItemsOrder\n    )\n\n    PgcNavItemsEditDialog(\n        show = showPgcNavItemsDialog,\n        onHideDialog = { showPgcNavItemsDialog = false },\n        initialOrderString = Prefs.pgcNavItemsOrder\n    )\n\n    LiveNavItemsEditDialog(\n        show = showLiveNavItemsDialog,\n        onHideDialog = { showLiveNavItemsDialog = false },\n        initialOrderString = Prefs.liveNavItemsOrder\n    )\n\n    DrawerNavItemsEditDialog(\n        show = showDrawerNavItemsDialog,\n        onHideDialog = { showDrawerNavItemsDialog = false },\n        initialOrderString = Prefs.drawerNavItemsOrder\n    )\n}\n\n@Composable\nprivate fun UIDensityDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    density: Float,\n    onDensityChange: (Float) -> Unit\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n    val defaultDensity by remember { mutableFloatStateOf(context.resources.displayMetrics.widthPixels / 960f) }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus(scope)\n    }\n\n    // 这里得采用固定的 Density，否则会导致更改 Density 时，对话框反复重新加载\n    CompositionLocalProvider(\n        LocalDensity provides Density(\n            density = defaultDensity,\n            fontScale = LocalDensity.current.fontScale\n        )\n    ) {\n        if (show) {\n            TvAlertDialog(\n                modifier = modifier,\n                onDismissRequest = { onHideDialog() },\n                title = { Text(text = stringResource(R.string.settings_ui_density_title)) },\n                text = {\n                    Column(\n                        modifier = Modifier\n                            .focusRequester(focusRequester)\n                            .focusable()\n                            .fillMaxWidth()\n                            .onPreviewKeyEvent {\n                                if (it.key == Key.DirectionUp || it.key == Key.DirectionDown) {\n                                    if (it.type == KeyEventType.KeyDown) {\n                                        var newDensity = if (it.key == Key.DirectionUp)\n                                            density + 0.1f else density - 0.1f\n                                        newDensity = (newDensity * 10).roundToInt() / 10f\n                                        if (newDensity < 0.5f) newDensity = 0.5f\n                                        if (newDensity > 5f) newDensity = 5f\n                                        onDensityChange(newDensity)\n                                    }\n                                }\n                                false\n                            },\n                        horizontalAlignment = Alignment.CenterHorizontally\n                    ) {\n                        Icon(imageVector = Icons.Rounded.ArrowDropUp, contentDescription = null)\n                        Text(text = \"$density\")\n                        Icon(imageVector = Icons.Rounded.ArrowDropDown, contentDescription = null)\n                    }\n                },\n                confirmButton = {}\n            )\n        }\n    }\n}\n\n@Composable\nfun ThemeTypeDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    themeType: ThemeType,\n    onThemeTypeChange: (ThemeType) -> Unit\n) {\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = { onHideDialog() },\n            title = { Text(text = stringResource(R.string.settings_ui_theme_type_title)) },\n            text = {\n                Column {\n                    ThemeType.entries.forEach {\n                        ListItem(\n                            selected = themeType == it,\n                            onClick = { onThemeTypeChange(it) },\n                            headlineContent = {\n                                Text(text = it.getDisplayName(LocalContext.current))\n                            },\n                            trailingContent = {\n                                RadioButton(\n                                    selected = themeType == it,\n                                    onClick = null\n                                )\n                            }\n                        )\n                    }\n                }\n            },\n            confirmButton = {}\n        )\n    }\n}\n\n@Composable\nfun InterfaceModeDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    interfaceMode: InterfaceMode,\n    onInterfaceModeChange: (InterfaceMode) -> Unit\n) {\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = { onHideDialog() },\n            title = { Text(text = stringResource(R.string.settings_ui_interface_mode_title)) },\n            text = {\n                Column {\n                    InterfaceMode.entries.forEach {\n                        ListItem(\n                            selected = interfaceMode == it,\n                            onClick = { onInterfaceModeChange(it) },\n                            headlineContent = {\n                                Text(text = it.getDisplayName(LocalContext.current))\n                            },\n                            trailingContent = {\n                                RadioButton(\n                                    selected = interfaceMode == it,\n                                    onClick = null\n                                )\n                            }\n                        )\n                    }\n                }\n            },\n            confirmButton = {}\n        )\n    }\n}\n\n@Composable\nfun NavSwitchModeDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    navSwitchMode: NavSwitchMode,\n    onNavSwitchModeChange: (NavSwitchMode) -> Unit\n) {\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = { onHideDialog() },\n            title = { Text(text = stringResource(R.string.settings_ui_nav_switch_mode_title)) },\n            text = {\n                Column {\n                    NavSwitchMode.entries.forEach {\n                        ListItem(\n                            selected = navSwitchMode == it,\n                            onClick = { onNavSwitchModeChange(it) },\n                            headlineContent = {\n                                Text(text = it.getDisplayName(LocalContext.current))\n                            },\n                            trailingContent = {\n                                RadioButton(\n                                    selected = navSwitchMode == it,\n                                    onClick = null\n                                )\n                            }\n                        )\n                    }\n                }\n            },\n            confirmButton = {}\n        )\n    }\n}\n\n@Preview\n@Composable\nfun UIDensityDialogPreview() {\n    val show by remember { mutableStateOf(true) }\n    var density by remember { mutableFloatStateOf(1.0f) }\n\n    BVTheme {\n        UIDensityDialog(\n            show = show,\n            onHideDialog = {},\n            density = density,\n            onDensityChange = { density = it }\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun ThemeTypeDialogPreview() {\n    val show by remember { mutableStateOf(true) }\n    val themeType by remember { mutableStateOf(ThemeType.Auto) }\n\n    BVTheme {\n        ThemeTypeDialog(\n            show = show,\n            onHideDialog = {},\n            themeType = themeType,\n            onThemeTypeChange = {}\n        )\n    }\n}\n\n@Composable\nprivate fun HomeNavItemsEditDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    initialOrderString: String\n) {\n    if (!show) return\n\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    // 解析初始配置（按显示顺序）\n    val initialConfigs = remember(initialOrderString) {\n        parseNavItemsOrderToConfig(initialOrderString)\n    }\n\n    // 当前配置状态\n    var navConfigs by remember { mutableStateOf(initialConfigs) }\n\n    // 当前选中的索引\n    var selectedIndex by remember { mutableIntStateOf(0) }\n\n    // 默认标签\n    var defaultTabOrdinal by remember { mutableIntStateOf(Prefs.defaultHomeTab) }\n\n    // 长按检测\n    var enterDownTime by remember { mutableStateOf(0L) }\n    var longPressHandled by remember { mutableStateOf(false) }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus(scope)\n    }\n\n    TvAlertDialog(\n        modifier = modifier,\n        onDismissRequest = {\n            // 关闭时自动保存\n            Prefs.defaultHomeTab = defaultTabOrdinal\n            saveNavConfigs(navConfigs, defaultTabOrdinal)\n            onHideDialog()\n        },\n        title = { Text(text = stringResource(R.string.settings_ui_home_nav_items_title)) },\n        text = {\n            Column(\n                modifier = Modifier\n                    .focusRequester(focusRequester)\n                    .focusable()\n                    .onPreviewKeyEvent {\n                        if (it.type == KeyEventType.KeyDown) {\n                            when (it.key) {\n                                Key.DirectionLeft -> {\n                                    if (selectedIndex > 0) {\n                                        navConfigs = navConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex - 1]\n                                            this[selectedIndex - 1] = temp\n                                        }\n                                        selectedIndex--\n                                    }\n                                    true\n                                }\n                                Key.DirectionRight -> {\n                                    if (selectedIndex < navConfigs.size - 1) {\n                                        navConfigs = navConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex + 1]\n                                            this[selectedIndex + 1] = temp\n                                        }\n                                        selectedIndex++\n                                    }\n                                    true\n                                }\n                                Key.DirectionUp -> {\n                                    if (selectedIndex > 0) selectedIndex--\n                                    true\n                                }\n                                Key.DirectionDown -> {\n                                    if (selectedIndex < navConfigs.size - 1) selectedIndex++\n                                    true\n                                }\n                                Key.Enter, Key.DirectionCenter -> {\n                                    if (enterDownTime == 0L) {\n                                        enterDownTime = System.currentTimeMillis()\n                                        longPressHandled = false\n                                    } else if (!longPressHandled && System.currentTimeMillis() - enterDownTime >= 600) {\n                                        // 长按：设为默认标签，并取消隐藏\n                                        longPressHandled = true\n                                        val config = navConfigs[selectedIndex]\n                                        defaultTabOrdinal = config.ordinal\n                                        if (config.hidden) {\n                                            navConfigs = navConfigs.toMutableList().apply {\n                                                this[selectedIndex] = config.copy(hidden = false)\n                                            }\n                                        }\n                                    }\n                                    true\n                                }\n                                else -> false\n                            }\n                        } else if (it.type == KeyEventType.KeyUp) {\n                            when (it.key) {\n                                Key.Enter, Key.DirectionCenter -> {\n                                    if (!longPressHandled && enterDownTime > 0L) {\n                                        // 短按：切换显示/隐藏（默认标签不可隐藏）\n                                        val config = navConfigs[selectedIndex]\n                                        if (config.ordinal != defaultTabOrdinal) {\n                                            navConfigs = navConfigs.toMutableList().apply {\n                                                this[selectedIndex] = config.copy(hidden = !config.hidden)\n                                            }\n                                        }\n                                    }\n                                    enterDownTime = 0L\n                                    longPressHandled = false\n                                    true\n                                }\n                                else -> false\n                            }\n                        } else false\n                    }\n            ) {\n                // 提示文字\n                Text(\n                    text = stringResource(R.string.settings_ui_home_nav_items_hint),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n                Spacer(modifier = Modifier.height(8.dp))\n\n                navConfigs.forEachIndexed { index, config ->\n                    val navItem = HomeTopNavItem.entries.getOrNull(config.ordinal)\n                    if (navItem != null) {\n                        val isSelected = index == selectedIndex\n                        val isDefaultHomeTab = config.ordinal == defaultTabOrdinal\n\n                        NavItemEditRow(\n                            title = navItem.getDisplayName(LocalContext.current),\n                            hidden = config.hidden,\n                            selected = isSelected,\n                            showDefaultTag = isDefaultHomeTab,\n                            onFocus = { selectedIndex = index }\n                        )\n                    }\n                }\n            }\n        },\n        confirmButton = {}\n    )\n}\n\n@Composable\nprivate fun UgcNavItemsEditDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    initialOrderString: String\n) {\n    if (!show) return\n\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    val initialConfigs = remember(initialOrderString) {\n        parseNavItemsOrderToConfig(initialOrderString, UgcTopNavItem.entries.size)\n    }\n    var navConfigs by remember { mutableStateOf(initialConfigs) }\n    var selectedIndex by remember { mutableIntStateOf(0) }\n\n    val listState = rememberLazyListState()\n\n    LaunchedEffect(selectedIndex, navConfigs.size) {\n        if (navConfigs.isEmpty()) return@LaunchedEffect\n\n        val layoutInfo = listState.layoutInfo\n        val viewportStart = layoutInfo.viewportStartOffset\n        val viewportEnd = layoutInfo.viewportEndOffset\n        val viewportSize = viewportEnd - viewportStart\n        if (viewportSize <= 0) return@LaunchedEffect\n\n        val visibleItems = layoutInfo.visibleItemsInfo\n        val visibleCount = visibleItems.size\n        if (visibleCount <= 0) return@LaunchedEffect\n\n        val firstVisible = listState.firstVisibleItemIndex\n        val selectedItemInfo = visibleItems.firstOrNull { it.index == selectedIndex }\n\n        // 1) 先保证“选中项完全可见”（尤其是最后一项，避免底部被截断）\n        if (selectedItemInfo != null) {\n            val itemStart = selectedItemInfo.offset\n            val itemEnd = itemStart + selectedItemInfo.size\n\n            if (itemStart < viewportStart) {\n                listState.animateScrollToItem(index = selectedIndex, scrollOffset = 0)\n                return@LaunchedEffect\n            }\n\n            if (itemEnd > viewportEnd) {\n                val bottomAlignedOffset = (viewportSize - selectedItemInfo.size).coerceAtLeast(0)\n                listState.animateScrollToItem(index = selectedIndex, scrollOffset = bottomAlignedOffset)\n                return@LaunchedEffect\n            }\n\n            // 2) 完全可见时，再按“到可见列表中间才开始滚动”的规则微调\n            val middleIndex = firstVisible + visibleCount / 2\n            if (selectedIndex > middleIndex) {\n                val maxFirstVisible = (navConfigs.size - visibleCount).coerceAtLeast(0)\n                val targetFirstVisible = (selectedIndex - visibleCount / 2)\n                    .coerceIn(0, maxFirstVisible)\n                if (targetFirstVisible != firstVisible) {\n                    listState.animateScrollToItem(index = targetFirstVisible)\n                }\n            }\n            return@LaunchedEffect\n        }\n\n        // 3) 选中项不在可见区域：直接滚动到“中间位置”附近\n        val maxFirstVisible = (navConfigs.size - visibleCount).coerceAtLeast(0)\n        val targetFirstVisible = (selectedIndex - visibleCount / 2)\n            .coerceIn(0, maxFirstVisible)\n        if (targetFirstVisible != firstVisible) {\n            listState.animateScrollToItem(index = targetFirstVisible)\n        }\n    }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus(scope)\n    }\n\n    TvAlertDialog(\n        modifier = modifier,\n        onDismissRequest = {\n            saveUgcNavConfigs(navConfigs)\n            onHideDialog()\n        },\n        title = { Text(text = stringResource(R.string.settings_ui_ugc_nav_items_title)) },\n        text = {\n            Column(\n                modifier = Modifier\n                    .focusRequester(focusRequester)\n                    .focusable()\n                    .onPreviewKeyEvent {\n                        if (it.type == KeyEventType.KeyDown) {\n                            when (it.key) {\n                                Key.DirectionLeft -> {\n                                    if (selectedIndex > 0) {\n                                        navConfigs = navConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex - 1]\n                                            this[selectedIndex - 1] = temp\n                                        }\n                                        selectedIndex--\n                                    }\n                                    true\n                                }\n                                Key.DirectionRight -> {\n                                    if (selectedIndex < navConfigs.size - 1) {\n                                        navConfigs = navConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex + 1]\n                                            this[selectedIndex + 1] = temp\n                                        }\n                                        selectedIndex++\n                                    }\n                                    true\n                                }\n                                Key.DirectionUp -> {\n                                    if (selectedIndex > 0) selectedIndex--\n                                    true\n                                }\n                                Key.DirectionDown -> {\n                                    if (selectedIndex < navConfigs.size - 1) selectedIndex++\n                                    true\n                                }\n                                Key.Enter, Key.DirectionCenter -> {\n                                    val config = navConfigs[selectedIndex]\n                                    navConfigs = navConfigs.toMutableList().apply {\n                                        this[selectedIndex] = config.copy(hidden = !config.hidden)\n                                    }\n                                    true\n                                }\n                                else -> false\n                            }\n                        }\n                        false\n                    }\n            ) {\n                Text(\n                    text = stringResource(R.string.settings_ui_ugc_nav_items_hint),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n                Spacer(modifier = Modifier.height(8.dp))\n\n                LazyColumn(\n                    modifier = Modifier.fillMaxWidth(),\n                    state = listState,\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    itemsIndexed(\n                        items = navConfigs,\n                        key = { index, config -> \"$index-nav-${config.ordinal}\" }\n                    ) { index, config ->\n                        val navItem = UgcTopNavItem.entries.getOrNull(config.ordinal) ?: return@itemsIndexed\n                        NavItemEditRow(\n                            title = navItem.getDisplayName(LocalContext.current),\n                            hidden = config.hidden,\n                            selected = index == selectedIndex,\n                            onFocus = { selectedIndex = index }\n                        )\n                    }\n                }\n            }\n        },\n        confirmButton = {}\n    )\n}\n\n@Composable\nprivate fun PgcNavItemsEditDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    initialOrderString: String\n) {\n    if (!show) return\n\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    val initialConfigs = remember(initialOrderString) {\n        parseNavItemsOrderToConfig(initialOrderString, PgcTopNavItem.entries.size)\n    }\n    var navConfigs by remember { mutableStateOf(initialConfigs) }\n    var selectedIndex by remember { mutableIntStateOf(0) }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus(scope)\n    }\n\n    TvAlertDialog(\n        modifier = modifier,\n        onDismissRequest = {\n            savePgcNavConfigs(navConfigs)\n            onHideDialog()\n        },\n        title = { Text(text = stringResource(R.string.settings_ui_pgc_nav_items_title)) },\n        text = {\n            Column(\n                modifier = Modifier\n                    .focusRequester(focusRequester)\n                    .focusable()\n                    .onPreviewKeyEvent {\n                        if (it.type == KeyEventType.KeyDown) {\n                            when (it.key) {\n                                Key.DirectionLeft -> {\n                                    if (selectedIndex > 0) {\n                                        navConfigs = navConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex - 1]\n                                            this[selectedIndex - 1] = temp\n                                        }\n                                        selectedIndex--\n                                    }\n                                    true\n                                }\n                                Key.DirectionRight -> {\n                                    if (selectedIndex < navConfigs.size - 1) {\n                                        navConfigs = navConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex + 1]\n                                            this[selectedIndex + 1] = temp\n                                        }\n                                        selectedIndex++\n                                    }\n                                    true\n                                }\n                                Key.DirectionUp -> {\n                                    if (selectedIndex > 0) selectedIndex--\n                                    true\n                                }\n                                Key.DirectionDown -> {\n                                    if (selectedIndex < navConfigs.size - 1) selectedIndex++\n                                    true\n                                }\n                                Key.Enter, Key.DirectionCenter -> {\n                                    val config = navConfigs[selectedIndex]\n                                    navConfigs = navConfigs.toMutableList().apply {\n                                        this[selectedIndex] = config.copy(hidden = !config.hidden)\n                                    }\n                                    true\n                                }\n                                else -> false\n                            }\n                        }\n                        false\n                    }\n            ) {\n                Text(\n                    text = stringResource(R.string.settings_ui_pgc_nav_items_hint),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n                Spacer(modifier = Modifier.height(8.dp))\n\n                navConfigs.forEachIndexed { index, config ->\n                    val navItem = PgcTopNavItem.entries.getOrNull(config.ordinal) ?: return@forEachIndexed\n                    NavItemEditRow(\n                        title = navItem.getDisplayName(LocalContext.current),\n                        hidden = config.hidden,\n                        selected = index == selectedIndex,\n                        onFocus = { selectedIndex = index }\n                    )\n                }\n            }\n        },\n        confirmButton = {}\n    )\n}\n\n@Composable\nprivate fun NavItemEditRow(\n    title: String,\n    hidden: Boolean,\n    selected: Boolean,\n    showDefaultTag: Boolean = false,\n    onFocus: () -> Unit\n) {\n    EditableNavRow(\n        selected = selected,\n        onFocus = onFocus,\n        headline = {\n            Text(\n                text = title,\n                textDecoration = if (hidden) TextDecoration.LineThrough else null,\n                color = if (selected) {\n                    MaterialTheme.colorScheme.background\n                } else if (hidden) {\n                    MaterialTheme.colorScheme.onSurfaceVariant\n                } else {\n                    MaterialTheme.colorScheme.onSurface\n                }\n            )\n        },\n        trailing = {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                if (showDefaultTag) {\n                    Text(\n                        text = stringResource(R.string.settings_ui_home_nav_default_tag),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = if (selected) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.primary\n                    )\n                }\n\n                // 隐藏状态标记\n                if (hidden) {\n                    Text(\n                        text = stringResource(R.string.settings_ui_home_nav_hidden),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = if (selected) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.error\n                    )\n                } else {\n                    Text(\n                        text = stringResource(R.string.settings_ui_home_nav_visible),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = if (selected) MaterialTheme.colorScheme.background else MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                }\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun EditableNavRow(\n    selected: Boolean,\n    onFocus: () -> Unit,\n    headline: @Composable () -> Unit,\n    trailing: @Composable () -> Unit\n) {\n    val shape = remember { RoundedCornerShape(12.dp) }\n    val bgColor = if (selected) MaterialTheme.colorScheme.onBackground else Color.Transparent\n\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .onFocusChanged { if (it.hasFocus) onFocus() }\n            .background(color = bgColor, shape = shape)\n            .padding(horizontal = 20.dp, vertical = 12.dp),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.SpaceBetween\n    ) {\n        Box(modifier = Modifier.weight(1f)) {\n            headline()\n        }\n        trailing()\n    }\n}\n\n/**\n * 保存导航项配置到 Prefs\n * 默认标签强制不隐藏\n */\nprivate fun saveNavConfigs(navConfigs: List<NavItemConfig>, defaultTabOrdinal: Int) {\n    val finalOrderString = navConfigs.joinToString(\",\") { config ->\n        val shouldHide = if (config.ordinal == defaultTabOrdinal) {\n            false  // 默认标签强制不隐藏\n        } else {\n            config.hidden\n        }\n        if (shouldHide) \"-${config.ordinal}\" else \"${config.ordinal}\"\n    }\n    Prefs.homeNavItemsOrder = finalOrderString\n}\n\nprivate fun saveUgcNavConfigs(navConfigs: List<NavItemConfig>) {\n    val finalOrderString = navConfigs.joinToString(\",\") { config ->\n        if (config.hidden) \"-${config.ordinal}\" else \"${config.ordinal}\"\n    }\n    Prefs.ugcNavItemsOrder = finalOrderString\n}\n\nprivate fun savePgcNavConfigs(navConfigs: List<NavItemConfig>) {\n    val finalOrderString = navConfigs.joinToString(\",\") { config ->\n        if (config.hidden) \"-${config.ordinal}\" else \"${config.ordinal}\"\n    }\n    Prefs.pgcNavItemsOrder = finalOrderString\n}\n\nprivate fun saveLiveNavConfigs(navConfigs: List<LiveNavItemConfig>) {\n    val finalOrderString = navConfigs.joinToString(\",\") { config ->\n        if (config.hidden) \"-${config.id}\" else config.id\n    }\n    Prefs.liveNavItemsOrder = finalOrderString\n}\n\n@Composable\nprivate fun LiveNavItemsEditDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    initialOrderString: String\n) {\n    if (!show) return\n\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    val cachedAreas = remember {\n        parseCachedLiveAreaGroups(Prefs.cachedLiveAreaGroups)\n    }\n    val isLoggedIn = remember { Prefs.isLogin }\n\n    val initialConfigs = remember(initialOrderString) {\n        parseLiveNavItemsOrderToConfig(initialOrderString, cachedAreas, isLoggedIn)\n    }\n    var navConfigs by remember { mutableStateOf(initialConfigs) }\n    var selectedIndex by remember { mutableIntStateOf(0) }\n\n    val listState = rememberLazyListState()\n\n    LaunchedEffect(selectedIndex, navConfigs.size) {\n        if (navConfigs.isEmpty()) return@LaunchedEffect\n\n        val layoutInfo = listState.layoutInfo\n        val viewportStart = layoutInfo.viewportStartOffset\n        val viewportEnd = layoutInfo.viewportEndOffset\n        val viewportSize = viewportEnd - viewportStart\n        if (viewportSize <= 0) return@LaunchedEffect\n\n        val visibleItems = layoutInfo.visibleItemsInfo\n        val visibleCount = visibleItems.size\n        if (visibleCount <= 0) return@LaunchedEffect\n\n        val firstVisible = listState.firstVisibleItemIndex\n        val selectedItemInfo = visibleItems.firstOrNull { it.index == selectedIndex }\n\n        if (selectedItemInfo != null) {\n            val itemStart = selectedItemInfo.offset\n            val itemEnd = itemStart + selectedItemInfo.size\n\n            if (itemStart < viewportStart) {\n                listState.animateScrollToItem(index = selectedIndex, scrollOffset = 0)\n                return@LaunchedEffect\n            }\n\n            if (itemEnd > viewportEnd) {\n                val bottomAlignedOffset = (viewportSize - selectedItemInfo.size).coerceAtLeast(0)\n                listState.animateScrollToItem(index = selectedIndex, scrollOffset = bottomAlignedOffset)\n                return@LaunchedEffect\n            }\n\n            val middleIndex = firstVisible + visibleCount / 2\n            if (selectedIndex > middleIndex) {\n                val maxFirstVisible = (navConfigs.size - visibleCount).coerceAtLeast(0)\n                val targetFirstVisible = (selectedIndex - visibleCount / 2)\n                    .coerceIn(0, maxFirstVisible)\n                if (targetFirstVisible != firstVisible) {\n                    listState.animateScrollToItem(index = targetFirstVisible)\n                }\n            }\n            return@LaunchedEffect\n        }\n\n        val maxFirstVisible = (navConfigs.size - visibleCount).coerceAtLeast(0)\n        val targetFirstVisible = (selectedIndex - visibleCount / 2)\n            .coerceIn(0, maxFirstVisible)\n        if (targetFirstVisible != firstVisible) {\n            listState.animateScrollToItem(index = targetFirstVisible)\n        }\n    }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus(scope)\n    }\n\n    TvAlertDialog(\n        modifier = modifier,\n        onDismissRequest = {\n            saveLiveNavConfigs(navConfigs)\n            onHideDialog()\n        },\n        title = { Text(text = stringResource(R.string.settings_ui_live_nav_items_title)) },\n        text = {\n            Column(\n                modifier = Modifier\n                    .focusRequester(focusRequester)\n                    .focusable()\n                    .onPreviewKeyEvent {\n                        if (it.type == KeyEventType.KeyDown) {\n                            when (it.key) {\n                                Key.DirectionLeft -> {\n                                    if (selectedIndex > 0) {\n                                        navConfigs = navConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex - 1]\n                                            this[selectedIndex - 1] = temp\n                                        }\n                                        selectedIndex--\n                                    }\n                                    true\n                                }\n                                Key.DirectionRight -> {\n                                    if (selectedIndex < navConfigs.size - 1) {\n                                        navConfigs = navConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex + 1]\n                                            this[selectedIndex + 1] = temp\n                                        }\n                                        selectedIndex++\n                                    }\n                                    true\n                                }\n                                Key.DirectionUp -> {\n                                    if (selectedIndex > 0) selectedIndex--\n                                    true\n                                }\n                                Key.DirectionDown -> {\n                                    if (selectedIndex < navConfigs.size - 1) selectedIndex++\n                                    true\n                                }\n                                Key.Enter, Key.DirectionCenter -> {\n                                    val config = navConfigs[selectedIndex]\n                                    navConfigs = navConfigs.toMutableList().apply {\n                                        this[selectedIndex] = config.copy(hidden = !config.hidden)\n                                    }\n                                    true\n                                }\n                                else -> false\n                            }\n                        }\n                        false\n                    }\n            ) {\n                if (cachedAreas.isEmpty()) {\n                    Text(\n                        text = stringResource(R.string.settings_ui_live_nav_items_empty_hint),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                } else {\n                    Text(\n                        text = stringResource(R.string.settings_ui_live_nav_items_hint),\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                }\n                Spacer(modifier = Modifier.height(8.dp))\n\n                LazyColumn(\n                    modifier = Modifier.fillMaxWidth(),\n                    state = listState,\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    itemsIndexed(\n                        items = navConfigs,\n                        key = { index, config -> \"$index-live-nav-${config.id}\" }\n                    ) { index, config ->\n                        NavItemEditRow(\n                            title = getLiveNavItemDisplayName(config.id, cachedAreas),\n                            hidden = config.hidden,\n                            selected = index == selectedIndex,\n                            onFocus = { selectedIndex = index }\n                        )\n                    }\n                }\n            }\n        },\n        confirmButton = {}\n    )\n}\n\n@Composable\nprivate fun DrawerNavItemsEditDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    initialOrderString: String\n) {\n    if (!show) return\n\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    val initialConfigs = remember(initialOrderString) {\n        parseDrawerNavItemsOrderToConfig(initialOrderString)\n    }\n    var navConfigs by remember { mutableStateOf(initialConfigs) }\n    var selectedIndex by remember { mutableIntStateOf(0) }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus(scope)\n    }\n\n    TvAlertDialog(\n        modifier = modifier,\n        onDismissRequest = {\n            saveDrawerNavConfigs(navConfigs)\n            onHideDialog()\n        },\n        title = { Text(text = stringResource(R.string.settings_ui_drawer_nav_items_title)) },\n        text = {\n            Column(\n                modifier = Modifier\n                    .focusRequester(focusRequester)\n                    .focusable()\n                    .onPreviewKeyEvent {\n                        if (it.type == KeyEventType.KeyDown) {\n                            when (it.key) {\n                                Key.DirectionLeft -> {\n                                    if (selectedIndex > 0) {\n                                        navConfigs = navConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex - 1]\n                                            this[selectedIndex - 1] = temp\n                                        }\n                                        selectedIndex--\n                                    }\n                                    true\n                                }\n                                Key.DirectionRight -> {\n                                    if (selectedIndex < navConfigs.size - 1) {\n                                        navConfigs = navConfigs.toMutableList().apply {\n                                            val temp = this[selectedIndex]\n                                            this[selectedIndex] = this[selectedIndex + 1]\n                                            this[selectedIndex + 1] = temp\n                                        }\n                                        selectedIndex++\n                                    }\n                                    true\n                                }\n                                Key.DirectionUp -> {\n                                    if (selectedIndex > 0) selectedIndex--\n                                    true\n                                }\n                                Key.DirectionDown -> {\n                                    if (selectedIndex < navConfigs.size - 1) selectedIndex++\n                                    true\n                                }\n                                Key.Enter, Key.DirectionCenter -> {\n                                    val config = navConfigs[selectedIndex]\n                                    navConfigs = navConfigs.toMutableList().apply {\n                                        this[selectedIndex] = config.copy(hidden = !config.hidden)\n                                    }\n                                    true\n                                }\n                                else -> false\n                            }\n                        }\n                        false\n                    }\n            ) {\n                Text(\n                    text = stringResource(R.string.settings_ui_drawer_nav_items_hint),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n                Spacer(modifier = Modifier.height(8.dp))\n\n                navConfigs.forEachIndexed { index, config ->\n                    val drawerItem = DrawerItem.entries.getOrNull(config.ordinal) ?: return@forEachIndexed\n                    NavItemEditRow(\n                        title = drawerItem.displayName,\n                        hidden = config.hidden,\n                        selected = index == selectedIndex,\n                        onFocus = { selectedIndex = index }\n                    )\n                }\n            }\n        },\n        confirmButton = {}\n    )\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/FavoriteScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user\n\nimport android.content.Context\nimport android.view.KeyEvent\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.FavoriteFolderMetadata\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.NavSwitchMode\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.manager.VideoUserActionManager\nimport dev.aaa1115910.bv.tv.component.TopNav\nimport dev.aaa1115910.bv.tv.component.TopNavItem\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.tv.util.stableItemKey\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.onDelayFocusChanged\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.user.FavoriteViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun FavoriteScreen(\n    modifier: Modifier = Modifier,\n    favoriteViewModel: FavoriteViewModel = koinViewModel(),\n    showPageTitle: Boolean = true\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val navSwitchMode by Prefs.navSwitchModeFlow.collectAsState(Prefs.navSwitchMode)\n    var currentIndex by remember { mutableIntStateOf(0) }\n    val showLargeTitle by remember { derivedStateOf { currentIndex < 4 } }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n    val focusRequester = remember { FocusRequester() }\n    val defaultFocusRequester = remember { FocusRequester() }\n    val gridDefaultFocusRequester = remember { FocusRequester() }\n    val gridFocusRestorer = rememberTvLazyListFocusRestorer(gridDefaultFocusRequester)\n    var focusOnTabs by remember { mutableStateOf(true) }\n    var focusOnGrid by remember { mutableStateOf(false) }\n    val lazyGridState = rememberLazyGridState()\n    val favoriteTopNavItems = favoriteViewModel.favoriteFolderMetadataList.map(::FavoriteFolderTopNavItem)\n\n    var deleteMode by remember { mutableStateOf(false) }\n    var showDeleteConfirmDialog by remember { mutableStateOf(false) }\n    var selectedVideo by remember { mutableStateOf<VideoCardData?>(null) }\n    var selectedIndex by remember { mutableIntStateOf(0) }\n\n    val focusRequesters = remember { mutableMapOf<Int, FocusRequester>() }\n    fun getFocusRequester(index: Int): FocusRequester {\n        return focusRequesters.getOrPut(index) { FocusRequester() }\n    }\n\n    val updateCurrentFavoriteFolder: (folderMetadata: FavoriteFolderMetadata) -> Unit =\n        { folderMetadata ->\n            favoriteViewModel.currentFavoriteFolderMetadata = folderMetadata\n            favoriteViewModel.favorites.clear()\n            favoriteViewModel.resetPageNumber()\n            favoriteViewModel.updateFolderItems(force = true)\n        }\n\n    BackHandler(\n        enabled = focusOnGrid && !deleteMode && !showPageTitle\n    ) {\n        scope.launch(Dispatchers.Main) {\n            lazyGridState.scrollToItem(0)\n            delay(100)\n            focusOnGrid = false\n            defaultFocusRequester.requestFocus()\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        if (favoriteViewModel.favoriteFolderMetadataList.isEmpty()) {\n            favoriteViewModel.clearData()\n            favoriteViewModel.updateFoldersInfo()\n            if (showPageTitle) {\n                delay(100)\n                defaultFocusRequester.requestFocus()\n            }\n        }\n    }\n\n    fun focusTopTabIfListEmpty() {\n        if (favoriteViewModel.favorites.isEmpty()) {\n            deleteMode = false\n            focusOnGrid = false\n            defaultFocusRequester.requestFocus(scope)\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            if (showPageTitle) {\n                Box(\n                    modifier = Modifier.padding(\n                        start = 48.dp,\n                        top = 24.dp,\n                        bottom = 8.dp,\n                        end = 48.dp\n                    )\n                ) {\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.SpaceBetween\n                    ) {\n                        Text(\n                            text = stringResource(R.string.user_homepage_favorite),\n                            fontSize = titleFontSize.sp\n                        )\n                        Column (\n                            modifier = Modifier.weight(1f),\n                        ){\n                            Text(\n                                modifier = Modifier.fillMaxWidth(),\n                                text = stringResource(\n                                    R.string.load_data_count,\n                                    favoriteViewModel.favorites.size\n                                ),\n                                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),\n                                fontSize = 11.sp,\n                                textAlign = TextAlign.End,\n                            )\n                            Text(\n                                modifier = Modifier.fillMaxWidth(),\n                                text = if (deleteMode) stringResource(R.string.delete_mode_action_hint) else stringResource(R.string.delete_mode_hint),\n                                color = if (deleteMode) Color.Red.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),\n                                fontSize = 11.sp,\n                                textAlign = TextAlign.End\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier.padding(innerPadding)\n        ) {\n            TopNav(\n                modifier = Modifier\n                    .focusRequester(defaultFocusRequester)\n                    .onFocusChanged { focusOnTabs = it.hasFocus }\n                    .onDelayFocusChanged(50) {\n                        if (focusOnTabs) {\n                            focusRequester.requestFocus()\n                        }\n                    },\n                paddingTop = 0.dp,\n                items = favoriteTopNavItems,\n                useSmallSize = !showPageTitle,\n                initialSelectedItem = favoriteTopNavItems.firstOrNull {\n                    it.folderMetadata == favoriteViewModel.currentFavoriteFolderMetadata\n                },\n                navSwitchMode = navSwitchMode,\n                tabFocusRequester = focusRequester,\n                onSelectedChanged = { selectedItem ->\n                    val folderMetadata = (selectedItem as FavoriteFolderTopNavItem).folderMetadata\n                    if (favoriteViewModel.currentFavoriteFolderMetadata != folderMetadata) {\n                        updateCurrentFavoriteFolder(folderMetadata)\n                    }\n                }\n            )\n\n            if (!showPageTitle) {\n                Text(\n                    modifier = Modifier\n                        .offset(y = (-6).dp)\n                        .fillMaxWidth()\n                        .height(14.dp),\n                    text = if (deleteMode) stringResource(R.string.delete_mode_action_hint) else stringResource(R.string.delete_mode_hint),\n                    color = if (deleteMode) Color.Red.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),\n                    fontSize = 11.sp,\n                    textAlign = TextAlign.End,\n                    lineHeight = 14.sp\n                )\n            }\n\n            ProvideListBringIntoViewSpec(padding = 24.dp) {\n                LazyVerticalGrid(\n                    modifier = gridFocusRestorer.containerModifier(\n                        Modifier\n                            .weight(1f)\n                            .blockDownFocusExitAtGridEnd(\n                                currentIndex = currentIndex,\n                                itemCount = favoriteViewModel.favorites.size,\n                                columnCount = 4\n                            )\n                            .onPreviewKeyEvent { keyEvent ->\n                                if (keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_UP &&\n                                    (keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_MENU ||\n                                     keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_DEL)\n                                ) {\n                                    deleteMode = !deleteMode\n                                    return@onPreviewKeyEvent true\n                                }\n                                if (deleteMode && keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_BACK) {\n                                    if (keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_UP) {\n                                        deleteMode = false\n                                    }\n                                    return@onPreviewKeyEvent true\n                                }\n                                false\n                            }\n                    ),\n                    state = lazyGridState,\n                    columns = GridCells.Fixed(4),\n                    contentPadding = PaddingValues(\n                        top = if (showPageTitle) 20.dp else 0.dp,\n                        bottom = 20.dp,\n                        start = 20.dp,\n                        end = 20.dp\n                    ),\n                    verticalArrangement = Arrangement.spacedBy(16.dp),\n                    horizontalArrangement = Arrangement.spacedBy(13.dp)\n                ) {\n                    itemsIndexed(\n                        items = favoriteViewModel.favorites,\n                        key = { _, history -> history.stableItemKey() }\n                    ) { index, history ->\n                        SmallVideoCard(\n                            modifier = gridFocusRestorer.firstItemModifier(index)\n                                .focusRequester(getFocusRequester(index)),\n                            data = history,\n                            onClick = {\n                                if (deleteMode) {\n                                    selectedVideo = history\n                                    selectedIndex = index\n                                    showDeleteConfirmDialog = true\n                                } else {\n                                    VideoInfoActivity.actionStart(context, history.avid)\n                                }\n                            },\n                            onLongClick = {\n                                if (deleteMode) {\n                                    val nextIndex = if (index < favoriteViewModel.favorites.size - 1) index + 1 else index - 1\n                                    if (nextIndex >= 0) runCatching { getFocusRequester(nextIndex).requestFocus() }\n                                    val aid = history.avid\n                                    val folderId = favoriteViewModel.currentFavoriteFolderMetadata?.id\n                                    scope.launch {\n                                        if (folderId != null) {\n                                            val success = VideoUserActionManager.delVideoFromFavoriteFolder(aid = aid, folderId = folderId)\n                                            if (success) {\n                                                favoriteViewModel.removeFavoriteFromList(aid)\n                                                focusTopTabIfListEmpty()\n                                                context.getString(R.string.favorite_delete_success).toast(context)\n                                            } else {\n                                                context.getString(R.string.favorite_delete_failed).toast(context)\n                                            }\n                                        }\n                                    }\n                                } else {\n                                    UpInfoActivity.actionStart(\n                                        context,\n                                        mid = history.upId,\n                                        name = history.upName,\n                                        face = history.upFace\n                                    )\n                                }\n                            },\n                            onFocus = {\n                                focusOnGrid = true\n                                currentIndex = index\n                                //预加载\n                                if (index + 12 > favoriteViewModel.favorites.size) {\n                                    favoriteViewModel.updateFolderItems()\n                                }\n                            }\n                        )\n                    }\n\n                    if (\n                        favoriteViewModel.favorites.isEmpty() &&\n                        favoriteViewModel.currentFavoriteFolderMetadata != null &&\n                        !favoriteViewModel.updatingFolders &&\n                        !favoriteViewModel.updatingFolderItems\n                    ) {\n                        item(span = { GridItemSpan(4) }) {\n                            Box(\n                                modifier = Modifier.fillMaxSize(),\n                                contentAlignment = Alignment.Center\n                            ) {\n                                Text(\n                                    text = stringResource(R.string.no_data),\n                                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    if (showDeleteConfirmDialog && selectedVideo != null) {\n        DeleteFavoriteConfirmDialog(\n            show = showDeleteConfirmDialog,\n            videoTitle = selectedVideo!!.title,\n            onConfirm = {\n                val nextIndex = if (selectedIndex < favoriteViewModel.favorites.size - 1) selectedIndex + 1 else selectedIndex - 1\n                if (nextIndex >= 0) runCatching { getFocusRequester(nextIndex).requestFocus() }\n                val aid = selectedVideo!!.avid\n                val folderId = favoriteViewModel.currentFavoriteFolderMetadata?.id\n                showDeleteConfirmDialog = false\n                selectedVideo = null\n                scope.launch {\n                    if (folderId != null) {\n                        val success = VideoUserActionManager.delVideoFromFavoriteFolder(aid = aid, folderId = folderId)\n                        if (success) {\n                            favoriteViewModel.removeFavoriteFromList(aid)\n                            focusTopTabIfListEmpty()\n                            context.getString(R.string.favorite_delete_success).toast(context)\n                        } else {\n                            context.getString(R.string.favorite_delete_failed).toast(context)\n                        }\n                    }\n                }\n            },\n            onDismiss = {\n                showDeleteConfirmDialog = false\n                scope.launch {\n                    runCatching { getFocusRequester(selectedIndex).requestFocus() }\n                }\n                selectedVideo = null\n            }\n        )\n    }\n}\n\nprivate data class FavoriteFolderTopNavItem(\n    val folderMetadata: FavoriteFolderMetadata\n) : TopNavItem {\n    override fun getDisplayName(context: Context): String {\n        return folderMetadata.title\n    }\n}\n\n@Composable\nprivate fun DeleteFavoriteConfirmDialog(\n    show: Boolean,\n    videoTitle: String,\n    onConfirm: () -> Unit,\n    onDismiss: () -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus()\n    }\n\n    TvAlertDialog(\n        onDismissRequest = onDismiss,\n        title = { Text(text = stringResource(R.string.favorite_delete_confirm_dialog_title)) },\n        text = {\n            Text(\n                text = stringResource(\n                    R.string.favorite_delete_confirm_dialog_text,\n                    videoTitle\n                )\n            )\n        },\n        confirmButton = {\n            Button(onClick = onConfirm) {\n                Text(text = stringResource(R.string.favorite_delete_confirm_dialog_confirm))\n            }\n        },\n        dismissButton = {\n            OutlinedButton(\n                modifier = Modifier.focusRequester(focusRequester),\n                onClick = onDismiss\n            ) {\n                Text(text = stringResource(R.string.favorite_delete_confirm_dialog_dismiss))\n            }\n        }\n    )\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/FollowScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.foundation.text.input.TextFieldLineLimits\nimport androidx.compose.foundation.text.input.TextFieldState\nimport androidx.compose.foundation.text.input.rememberTextFieldState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.OutlinedTextFieldDefaults\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Border\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.component.LoadingTip\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.viewmodel.user.FollowViewModel\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun FollowScreen(\n    modifier: Modifier = Modifier,\n    followViewModel: FollowViewModel = koinViewModel()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val defaultFocusRequester = remember { FocusRequester() }\n    val gridFocusRestorer = rememberTvLazyListFocusRestorer(defaultFocusRequester)\n\n    var currentIndex by remember { mutableIntStateOf(0) }\n    val showLargeTitle by remember { derivedStateOf { currentIndex < 3 } }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n    val searchState: TextFieldState = rememberTextFieldState()\n    // 根据搜索关键字筛选用户（用户名或签名包含关键字，不区分大小写）\n    val filteredUsers by remember {\n        derivedStateOf {\n            val keyword = searchState.text.toString().trim()\n            if (keyword.isEmpty()) followViewModel.followedUsers\n            else {\n                // 按字符匹配：关键字中的每个非空白字符都必须在用户名或签名中出现\n                val chars = keyword.filter { !it.isWhitespace() }.map { it.toString() }\n                followViewModel.followedUsers.filter { up ->\n                    chars.all { ch ->\n                        up.name.contains(ch, ignoreCase = true) ||\n                                up.sign.contains(ch, ignoreCase = true)\n                    }\n                }\n            }\n        }\n    }\n\n    LaunchedEffect(followViewModel.updating) {\n        if (!followViewModel.updating) {\n            defaultFocusRequester.requestFocus(scope)\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            Box(\n                modifier = Modifier.padding(\n                    start = 48.dp,\n                    top = 24.dp,\n                    bottom = 8.dp,\n                    end = 48.dp\n                )\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Text(\n                        text = stringResource(R.string.user_homepage_follow),\n                        fontSize = titleFontSize.sp\n                    )\n                    OutlinedTextField(\n                        state = searchState,\n                        enabled = !followViewModel.updating,\n                        modifier = Modifier\n                            .offset(y = (-2).dp)\n                            .width(258.dp)\n                            .height(36.dp),\n                        shape = MaterialTheme.shapes.small,\n                        contentPadding = PaddingValues(\n                            start = 0.dp,\n                            end = 8.dp,\n                            top = 4.dp,\n                            bottom = 4.dp\n                        ),\n                        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n                        lineLimits = TextFieldLineLimits.SingleLine,\n                        colors = OutlinedTextFieldDefaults.colors(\n                            focusedBorderColor = MaterialTheme.colorScheme.inverseSurface,\n                            cursorColor = MaterialTheme.colorScheme.inverseSurface\n                        ),\n                        leadingIcon = (@Composable {\n                            Icon(\n                                imageVector = Icons.Filled.Search,\n                                contentDescription = null\n                            )\n                        })\n                    )\n                    Text(\n                        modifier = Modifier.offset(y = 8.dp),\n                        text = stringResource(\n                            R.string.load_data_count_no_more,\n                            filteredUsers.size\n                        ),\n                        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                    )\n                }\n            }\n        }\n    ) { innerPadding ->\n        LazyVerticalGrid(\n            modifier = gridFocusRestorer.containerModifier(Modifier.padding(innerPadding)),\n            columns = GridCells.Fixed(3),\n            contentPadding = PaddingValues(20.dp),\n            verticalArrangement = Arrangement.spacedBy(18.dp),\n            horizontalArrangement = Arrangement.spacedBy(20.dp)\n        ) {\n            if (!followViewModel.updating) {\n                itemsIndexed(\n                    items = filteredUsers,\n                    key = { index, up -> \"$index-up-${up.mid}\" }\n                ) { index, up ->\n                    val upCardModifier = gridFocusRestorer.firstItemModifier(index)\n                    UpCard(\n                        modifier = upCardModifier,\n                        face = up.avatar,\n                        sign = up.sign,\n                        username = up.name,\n                        onFocusChange = {\n                            if (it) currentIndex = index\n                        },\n                        onClick = {\n                            UpInfoActivity.actionStart(\n                                context = context,\n                                mid = up.mid,\n                                name = up.name,\n                                face = up.avatar\n                            )\n                        }\n                    )\n                }\n            } else {\n                item(\n                    span = { GridItemSpan(3) }\n                ) {\n                    Box(\n                        modifier = Modifier.fillMaxSize(),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        LoadingTip()\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun UpCard(\n    modifier: Modifier = Modifier,\n    face: String,\n    sign: String,\n    username: String,\n    onFocusChange: (hasFocus: Boolean) -> Unit,\n    onClick: () -> Unit,\n    onLongClick: () -> Unit = {}\n) {\n    Surface(\n        modifier = modifier\n            .onFocusChanged { onFocusChange(it.hasFocus) }\n            .size(280.dp, 100.dp),\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surface,\n            focusedContainerColor = MaterialTheme.colorScheme.surface,\n            pressedContainerColor = MaterialTheme.colorScheme.surface\n        ),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        border = ClickableSurfaceDefaults.border(\n            focusedBorder = Border(\n                border = BorderStroke(width = 3.dp, color = MaterialTheme.colorScheme.border),\n                shape = MaterialTheme.shapes.medium\n            )\n        ),\n        onClick = onClick,\n        onLongClick = onLongClick\n    ) {\n        Row(\n            modifier = Modifier.fillMaxSize(),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            androidx.compose.material3.Surface(\n                modifier = Modifier\n                    .padding(start = 12.dp, end = 8.dp)\n                    .size(48.dp)\n                    .clip(CircleShape),\n                color = Color.White\n            ) {\n                AsyncImage(\n                    modifier = Modifier\n                        .size(48.dp)\n                        .clip(CircleShape),\n                    model = face,\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n            }\n            Column {\n                Text(\n                    text = username,\n                    style = MaterialTheme.typography.titleLarge,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n                Text(\n                    text = sign,\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis\n                )\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nfun UpCardPreview() {\n    BVTheme {\n        UpCard(\n            face = \"\",\n            sign = \"一只业余做翻译的Klei迷，动态区UP（自称），缺氧官中反馈可私信\",\n            username = \"username\",\n            onFocusChange = {},\n            onClick = {}\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/FollowingSeasonFilter.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Done\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.ExperimentalTvMaterial3Api\nimport androidx.tv.material3.FilterChip\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonStatus\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.util.getDisplayName\nimport dev.aaa1115910.bv.util.ifElse\n\n@Composable\nfun FollowingSeasonFilter(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideFilter: () -> Unit,\n    selectedType: FollowingSeasonType,\n    selectedStatus: FollowingSeasonStatus,\n    onSelectedTypeChange: (FollowingSeasonType) -> Unit,\n    onSelectedStatusChange: (FollowingSeasonStatus) -> Unit\n) {\n    val context = LocalContext.current\n    val row1FocusRequester = remember { FocusRequester() }\n    val row2FocusRequester = remember { FocusRequester() }\n\n    val filterRowSpace = 8.dp\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = onHideFilter,\n            title = { Text(text = stringResource(R.string.filter_dialog_title)) },\n            text = {\n                Column(\n                    verticalArrangement = Arrangement.spacedBy(filterRowSpace)\n                ) {\n                    LazyRow(\n                        modifier = Modifier\n                            .focusRestorer(row1FocusRequester),\n                        horizontalArrangement = Arrangement.spacedBy(filterRowSpace),\n                        contentPadding = PaddingValues(horizontal = filterRowSpace)\n                    ) {\n                        itemsIndexed(\n                            items = FollowingSeasonType.entries,\n                            key = { index, type -> \"$index-type-${type.name}\" }\n                        ) { _, type ->\n                            FilterDialogFilterChip(\n                                modifier = Modifier\n                                    .ifElse(\n                                        type == selectedType,\n                                        Modifier.focusRequester(row1FocusRequester)\n                                    ),\n                                selected = type == selectedType,\n                                onClick = { onSelectedTypeChange(type) },\n                                label = { Text(text = type.getDisplayName(context)) },\n                            )\n                        }\n                    }\n                    LazyRow(\n                        modifier = Modifier\n                            .focusRestorer(row2FocusRequester),\n                        horizontalArrangement = Arrangement.spacedBy(filterRowSpace),\n                        contentPadding = PaddingValues(horizontal = filterRowSpace)\n                    ) {\n                        itemsIndexed(\n                            items = FollowingSeasonStatus.entries,\n                            key = { index, status -> \"$index-status-${status.name}\" }\n                        ) { _, status ->\n                            FilterDialogFilterChip(\n                                modifier = Modifier\n                                    .ifElse(\n                                        status == selectedStatus,\n                                        Modifier.focusRequester(row2FocusRequester)\n                                    ),\n                                selected = status == selectedStatus,\n                                onClick = { onSelectedStatusChange(status) },\n                                label = { Text(text = status.getDisplayName(context)) }\n                            )\n                        }\n                    }\n                }\n            },\n            confirmButton = {}\n        )\n    }\n\n    BackHandler(\n        enabled = show,\n        onBack = onHideFilter\n    )\n}\n\n@OptIn(ExperimentalTvMaterial3Api::class)\n@Composable\nprivate fun FilterDialogFilterChip(\n    modifier: Modifier = Modifier,\n    selected: Boolean,\n    onClick: () -> Unit,\n    label: @Composable () -> Unit,\n) {\n    FilterChip(\n        modifier = modifier,\n        selected = selected,\n        onClick = onClick,\n        content = label,\n        leadingIcon = {\n            Row {\n                AnimatedVisibility(visible = selected) {\n                    Icon(\n                        modifier = Modifier.size(20.dp),\n                        imageVector = Icons.Rounded.Done,\n                        contentDescription = null\n                    )\n                }\n            }\n        }\n    )\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/FollowingSeasonScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user\n\nimport android.view.KeyEvent\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeason\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonStatus\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonType\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.videocard.SeasonCard\nimport dev.aaa1115910.bv.entity.carddata.SeasonCardData\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.activities.video.SeasonInfoActivity\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.getDisplayName\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.util.resizedImageUrl\nimport dev.aaa1115910.bv.viewmodel.user.FollowingSeasonViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun FollowingSeasonScreen(\n    modifier: Modifier = Modifier,\n    followingSeasonViewModel: FollowingSeasonViewModel = koinViewModel(),\n    showPageTitle: Boolean = true,\n    topTabFocusRequester: FocusRequester? = null\n) {\n    val context = LocalContext.current\n    val logger = KotlinLogging.logger { }\n    val scope = rememberCoroutineScope()\n    val gridFocusRestorer = rememberTvLazyListFocusRestorer()\n\n    var currentIndex by remember { mutableIntStateOf(0) }\n    val showLargeTitle by remember { derivedStateOf { currentIndex < 6 } }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n    val subtitleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 36f else 24f,\n        label = \"subtitle font size\"\n    )\n\n    var showFilter by remember { mutableStateOf(false) }\n\n    var deleteMode by remember { mutableStateOf(false) }\n    var showDeleteConfirmDialog by remember { mutableStateOf(false) }\n    var selectedSeason by remember { mutableStateOf<FollowingSeason?>(null) }\n    var selectedIndex by remember { mutableIntStateOf(0) }\n    var focusTopTabWhenListEmpty by remember { mutableStateOf(false) }\n\n    val focusRequesters = remember { mutableMapOf<Int, FocusRequester>() }\n    fun getFocusRequester(index: Int): FocusRequester {\n        return focusRequesters.getOrPut(index) { FocusRequester() }\n    }\n\n    val followingSeasons = followingSeasonViewModel.followingSeasons\n    var followingSeasonType by remember { mutableStateOf(followingSeasonViewModel.followingSeasonType) }\n    var followingSeasonStatus by remember { mutableStateOf(followingSeasonViewModel.followingSeasonStatus) }\n    val noMore = followingSeasonViewModel.noMore\n\n    val updateType: (FollowingSeasonType) -> Unit = {\n        if (followingSeasonType != it) {\n            followingSeasonType = it\n            followingSeasonViewModel.followingSeasonType = it\n            followingSeasonViewModel.clearData()\n            followingSeasonViewModel.loadMore()\n        }\n    }\n\n    val updateStatus: (FollowingSeasonStatus) -> Unit = {\n        if (followingSeasonStatus != it) {\n            followingSeasonStatus = it\n            followingSeasonViewModel.followingSeasonStatus = it\n            followingSeasonViewModel.clearData()\n            followingSeasonViewModel.loadMore()\n        }\n    }\n\n    val onLongClickSeason: (FollowingSeason, Int) -> Unit = { season, index ->\n        if (deleteMode) {\n            if (topTabFocusRequester != null) {\n                focusTopTabWhenListEmpty = true\n            }\n            val nextIndex = if (index < followingSeasons.size - 1) index + 1 else index - 1\n            if (nextIndex >= 0) runCatching { getFocusRequester(nextIndex).requestFocus() }\n            followingSeasonViewModel.unfollowSeason(seasonId = season.seasonId)\n        } else {\n            showFilter = true\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        if (followingSeasons.isEmpty()) {\n            logger.fInfo { \"Start update search result because filter updated\" }\n            followingSeasonViewModel.clearData()\n            followingSeasonViewModel.loadMore()\n        }\n    }\n\n    LaunchedEffect(followingSeasonViewModel.deleting, followingSeasons.size, focusTopTabWhenListEmpty) {\n        if (!focusTopTabWhenListEmpty || followingSeasonViewModel.deleting) return@LaunchedEffect\n        focusTopTabWhenListEmpty = false\n        if (followingSeasons.isEmpty()) {\n            deleteMode = false\n            topTabFocusRequester?.requestFocus(scope)\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            if (showPageTitle) {\n                Box(\n                    modifier = Modifier.padding(\n                        start = 48.dp,\n                        top = 24.dp,\n                        bottom = 8.dp,\n                        end = 48.dp\n                    )\n                ) {\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        verticalAlignment = Alignment.Bottom,\n                        horizontalArrangement = Arrangement.SpaceBetween\n                    ) {\n                        Row(\n                            horizontalArrangement = Arrangement.spacedBy(8.dp),\n                            verticalAlignment = Alignment.Bottom\n                        ) {\n                            Text(\n                                text = stringResource(R.string.title_activity_following_season),\n                                fontSize = titleFontSize.sp\n                            )\n                            Text(\n                                text = followingSeasonType.getDisplayName(context),\n                                fontSize = subtitleFontSize.sp\n                            )\n                            Text(\n                                text = \"(${followingSeasonStatus.getDisplayName(context)})\",\n                                fontSize = subtitleFontSize.sp\n                            )\n                        }\n                        Column(\n                            horizontalAlignment = Alignment.End,\n                        ) {\n                            Text(\n                                text = if (deleteMode) stringResource(R.string.delete_mode_action_hint) else stringResource(R.string.following_season_hint),\n                                color = if (deleteMode) Color.Red.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),\n                                fontSize = 11.sp\n                            )\n                            if (noMore) {\n                                Text(\n                                    text = stringResource(\n                                        R.string.load_data_count_no_more,\n                                        followingSeasonViewModel.followingSeasons.size\n                                    ),\n                                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),\n                                    fontSize = 11.sp\n                                )\n                            } else {\n                                Text(\n                                    text = stringResource(\n                                        R.string.load_data_count,\n                                        followingSeasonViewModel.followingSeasons.size\n                                    ),\n                                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),\n                                    fontSize = 11.sp\n                                )\n                            }\n                        }\n                    }\n                }\n            } else {\n                Row(\n                    modifier = Modifier.fillMaxWidth().padding(end = 24.dp),\n                    horizontalArrangement = Arrangement.End\n                ) {\n                    Text(\n                        text = if (deleteMode) stringResource(R.string.delete_mode_action_hint) else stringResource(R.string.following_season_hint),\n                        color = if (deleteMode) Color.Red.copy(alpha = 0.8f) else androidx.tv.material3.MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),\n                        fontSize = 11.sp\n                    )\n                }\n            }\n        }\n    ) { innerPadding ->\n        ProvideListBringIntoViewSpec {\n            LazyVerticalGrid(\n                modifier = gridFocusRestorer.containerModifier(\n                    Modifier\n                        .padding(innerPadding)\n                        .blockDownFocusExitAtGridEnd(\n                            currentIndex = currentIndex,\n                            itemCount = followingSeasons.size,\n                            columnCount = 6\n                        )\n                        .onPreviewKeyEvent { keyEvent ->\n                            if (keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_UP &&\n                                (keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_MENU ||\n                                 keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_DEL)\n                            ) {\n                                deleteMode = !deleteMode\n                                return@onPreviewKeyEvent true\n                            }\n                            if (deleteMode && keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_BACK) {\n                                if (keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_UP) {\n                                    deleteMode = false\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                            false\n                        }\n                ),\n                columns = GridCells.Fixed(6),\n                contentPadding = PaddingValues(24.dp),\n                verticalArrangement = Arrangement.spacedBy(20.dp),\n                horizontalArrangement = Arrangement.spacedBy(20.dp)\n            ) {\n                itemsIndexed(\n                    items = followingSeasons,\n                    key = { _, followingSeason -> \"season-${followingSeason.seasonId}\" }\n                ) { index, followingSeason ->\n                    SeasonCard(\n                        modifier = gridFocusRestorer.firstItemModifier(index)\n                            .focusRequester(getFocusRequester(index)),\n                        data = SeasonCardData(\n                            seasonId = followingSeason.seasonId,\n                            title = followingSeason.title,\n                            cover = followingSeason.cover.resizedImageUrl(ImageSize.SeasonCoverThumbnail),\n                            rating = null\n                        ),\n                        onFocus = {\n                            currentIndex = index\n                            if (index + 12 > followingSeasons.size) {\n                                println(\"load more by focus\")\n                                followingSeasonViewModel.loadMore()\n                            }\n                        },\n                        onClick = {\n                            if (deleteMode) {\n                                selectedSeason = followingSeason\n                                selectedIndex = index\n                                showDeleteConfirmDialog = true\n                            } else {\n                                SeasonInfoActivity.actionStart(\n                                    context = context,\n                                    seasonId = followingSeason.seasonId,\n                                    proxyArea = ProxyArea.checkProxyArea(followingSeason.title)\n                                )\n                            }\n                        },\n                        onLongClick = { onLongClickSeason(followingSeason, index) }\n                    )\n                }\n                if (followingSeasons.isEmpty() && noMore) {\n                    item(\n                        span = { GridItemSpan(6) }\n                    ) {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center\n                        ) {\n                            Column(\n                                horizontalAlignment = Alignment.CenterHorizontally,\n                                verticalArrangement = Arrangement.spacedBy(8.dp)\n                            ) {\n                                Text(text = stringResource(R.string.no_data))\n                                OutlinedButton(onClick = { showFilter = true }) {\n                                    Text(text = stringResource(R.string.filter_dialog_open_tip_click))\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    FollowingSeasonFilter(\n        show = showFilter,\n        onHideFilter = { showFilter = false },\n        selectedType = followingSeasonType,\n        selectedStatus = followingSeasonStatus,\n        onSelectedTypeChange = updateType,\n        onSelectedStatusChange = updateStatus\n    )\n\n    if (showDeleteConfirmDialog && selectedSeason != null) {\n        DeleteFollowingSeasonConfirmDialog(\n            show = showDeleteConfirmDialog,\n            seasonTitle = selectedSeason!!.title,\n            onConfirm = {\n                if (topTabFocusRequester != null) {\n                    focusTopTabWhenListEmpty = true\n                }\n                val nextIndex = if (selectedIndex < followingSeasons.size - 1) selectedIndex + 1 else selectedIndex - 1\n                if (nextIndex >= 0) runCatching { getFocusRequester(nextIndex).requestFocus() }\n                followingSeasonViewModel.unfollowSeason(seasonId = selectedSeason!!.seasonId)\n                showDeleteConfirmDialog = false\n                selectedSeason = null\n            },\n            onDismiss = {\n                showDeleteConfirmDialog = false\n                scope.launch {\n                    runCatching { getFocusRequester(selectedIndex).requestFocus() }\n                }\n                selectedSeason = null\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun DeleteFollowingSeasonConfirmDialog(\n    show: Boolean,\n    seasonTitle: String,\n    onConfirm: () -> Unit,\n    onDismiss: () -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus()\n    }\n\n    TvAlertDialog(\n        onDismissRequest = onDismiss,\n        title = { Text(text = stringResource(R.string.following_season_delete_confirm_dialog_title)) },\n        text = {\n            Text(\n                text = stringResource(\n                    R.string.following_season_delete_confirm_dialog_text,\n                    seasonTitle\n                )\n            )\n        },\n        confirmButton = {\n            Button(onClick = onConfirm) {\n                Text(text = stringResource(R.string.following_season_delete_confirm_dialog_confirm))\n            }\n        },\n        dismissButton = {\n            OutlinedButton(\n                modifier = Modifier.focusRequester(focusRequester),\n                onClick = onDismiss\n            ) {\n                Text(text = stringResource(R.string.following_season_delete_confirm_dialog_dismiss))\n            }\n        }\n    )\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/HistoryScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user\n\nimport android.view.KeyEvent\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.activities.video.SeasonInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.tv.util.stableItemKey\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport dev.aaa1115910.bv.viewmodel.user.HistoryViewModel\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.koinInject\n\n@Composable\nfun HistoryScreen(\n    modifier: Modifier = Modifier,\n    historyViewModel: HistoryViewModel = koinViewModel(),\n    showPageTitle: Boolean = true,\n    topTabFocusRequester: FocusRequester? = null\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val videoInfoRepository: VideoInfoRepository = koinInject()\n    val listFocusRestorer = rememberTvLazyListFocusRestorer()\n    val lazyGridState = rememberLazyGridState()\n    var currentIndex by remember { mutableIntStateOf(0) }\n    val showLargeTitle by remember { derivedStateOf { currentIndex < 4 } }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n\n    var deleteMode by remember { mutableStateOf(false) }\n    var showDeleteConfirmDialog by remember { mutableStateOf(false) }\n    var showClearConfirmDialog by remember { mutableStateOf(false) }\n    var selectedVideo by remember { mutableStateOf<VideoCardData?>(null) }\n    var selectedIndex by remember { mutableIntStateOf(0) }\n    var focusTopTabWhenListEmpty by remember { mutableStateOf(false) }\n\n    val focusRequesters = remember { mutableMapOf<Int, FocusRequester>() }\n    fun getFocusRequester(index: Int): FocusRequester {\n        return focusRequesters.getOrPut(index) { FocusRequester() }\n    }\n\n    LaunchedEffect(Unit) {\n        if (historyViewModel.histories.isEmpty()) {\n            historyViewModel.clearData()\n            historyViewModel.update()\n        }\n    }\n\n    LaunchedEffect(historyViewModel.deleting, historyViewModel.histories.size, focusTopTabWhenListEmpty) {\n        if (!focusTopTabWhenListEmpty || historyViewModel.deleting) return@LaunchedEffect\n        focusTopTabWhenListEmpty = false\n        if (historyViewModel.histories.isEmpty()) {\n            deleteMode = false\n            topTabFocusRequester?.requestFocus(scope)\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            if (showPageTitle) {\n                Box(\n                    modifier = Modifier.padding(\n                        start = 48.dp,\n                        top = 24.dp,\n                        bottom = 8.dp,\n                        end = 48.dp\n                    )\n                ) {\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        verticalAlignment = Alignment.Bottom,\n                        horizontalArrangement = Arrangement.SpaceBetween\n                    ) {\n                        Text(\n                            text = stringResource(R.string.user_homepage_recent),\n                            fontSize = titleFontSize.sp\n                        )\n                        if (historyViewModel.noMore) {\n                            Text(\n                                text = stringResource(\n                                    R.string.load_data_count_no_more,\n                                    historyViewModel.histories.size\n                                ),\n                                color = Color.White.copy(alpha = 0.6f)\n                            )\n                        } else {\n                            Text(\n                                text = stringResource(\n                                    R.string.load_data_count,\n                                    historyViewModel.histories.size\n                                ),\n                                color = Color.White.copy(alpha = 0.6f)\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    ) { innerPadding ->\n        Column(modifier = Modifier.padding(innerPadding)) {\n            Text(\n                modifier = Modifier.fillMaxWidth().offset(x = (-20).dp, y = (-2).dp),\n                text = if (deleteMode) stringResource(R.string.delete_mode_action_hint) else stringResource(R.string.delete_mode_hint),\n                color = if (deleteMode) Color.Red.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),\n                fontSize = 11.sp,\n                textAlign = TextAlign.End\n            )\n            ProvideListBringIntoViewSpec(padding = 24.dp) {\n                LazyVerticalGrid(\n                    modifier = listFocusRestorer.containerModifier(\n                        Modifier\n                            .blockDownFocusExitAtGridEnd(\n                            currentIndex = currentIndex,\n                            itemCount = historyViewModel.histories.size,\n                            columnCount = 4\n                        )\n                        .onPreviewKeyEvent { keyEvent ->\n                            if (keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_UP &&\n                                (keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_MENU ||\n                                 keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_DEL)\n                            ) {\n                                deleteMode = !deleteMode\n                                return@onPreviewKeyEvent true\n                            }\n                            if (deleteMode && keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_BACK) {\n                                if (keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_UP) {\n                                    deleteMode = false\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                            false\n                        }\n                ),\n                columns = GridCells.Fixed(4),\n                state = lazyGridState,\n                contentPadding = PaddingValues(\n                    top = if (showPageTitle) 20.dp else 4.dp,\n                    bottom = 20.dp,\n                    start = 20.dp,\n                    end = 20.dp\n                ),\n                verticalArrangement = Arrangement.spacedBy(16.dp),\n                horizontalArrangement = Arrangement.spacedBy(13.dp)\n            ) {\n                itemsIndexed(\n                    items = historyViewModel.histories,\n                    key = { _, history -> history.historyKid ?: history.hashCode() }\n                ) { index, history ->\n                    Box(\n                        contentAlignment = Alignment.Center\n                    ) {\n                        SmallVideoCard(\n                            modifier = listFocusRestorer.firstItemModifier(index)\n                                .focusRequester(getFocusRequester(index)),\n                            data = history,\n                            onClick = {\n                                if (deleteMode) {\n                                    selectedVideo = history\n                                    selectedIndex = index\n                                    showDeleteConfirmDialog = true\n                                } else {\n                                    videoInfoRepository.preloadedVideoList.clear()\n                                    videoInfoRepository.preloadedVideoList.addAll(historyViewModel.histories)\n                                    if (history.jumpToSeason) {\n                                        SeasonInfoActivity.actionStart(\n                                            context = context,\n                                            epId = history.epId,\n                                            seasonId = history.seasonId,\n                                            proxyArea = ProxyArea.checkProxyArea(history.title)\n                                        )\n                                    } else {\n                                        VideoInfoActivity.actionStart(\n                                            context = context,\n                                            aid = history.avid,\n                                            proxyArea = ProxyArea.checkProxyArea(history.title)\n                                        )\n                                    }\n                                }\n                            },\n                            onLongClick = {\n                                if (deleteMode) {\n                                    selectedIndex = index\n                                    showClearConfirmDialog = true\n                                } else {\n                                    UpInfoActivity.actionStart(\n                                        context,\n                                        mid = history.upId,\n                                        name = history.upName,\n                                        face = history.upFace\n                                    )\n                                }\n                            },\n                            onFocus = {\n                                currentIndex = index\n                                //预加载\n                                if (index + 12 > historyViewModel.histories.size) {\n                                    historyViewModel.update()\n                                }\n                            }\n                        )\n                    }\n                }\n\n                if (historyViewModel.histories.isEmpty() && historyViewModel.noMore) {\n                    item(span = { GridItemSpan(4) }) {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center\n                        ) {\n                            Text(\n                                text = stringResource(R.string.no_data),\n                                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                            )\n                        }\n                    }\n                }\n            }\n        }\n        }\n    }\n\n    if (showDeleteConfirmDialog && selectedVideo != null) {\n        DeleteHistoryConfirmDialog(\n            show = showDeleteConfirmDialog,\n            videoTitle = selectedVideo!!.title,\n            onConfirm = {\n                if (topTabFocusRequester != null) {\n                    focusTopTabWhenListEmpty = true\n                }\n                val nextIndex = if (selectedIndex < historyViewModel.histories.size - 1) selectedIndex + 1 else selectedIndex - 1\n                if (nextIndex >= 0) runCatching { getFocusRequester(nextIndex).requestFocus() }\n                historyViewModel.deleteHistory(\n                    business = selectedVideo!!.historyBusiness,\n                    kid = selectedVideo!!.historyKid\n                )\n                showDeleteConfirmDialog = false\n                selectedVideo = null\n            },\n            onDismiss = {\n                showDeleteConfirmDialog = false\n                scope.launch {\n                    runCatching { getFocusRequester(selectedIndex).requestFocus() }\n                }\n                selectedVideo = null\n            }\n        )\n    }\n\n    if (showClearConfirmDialog) {\n        ClearHistoryConfirmDialog(\n            show = showClearConfirmDialog,\n            onConfirm = {\n                if (topTabFocusRequester != null) {\n                    focusTopTabWhenListEmpty = true\n                }\n                historyViewModel.clearHistory()\n                deleteMode = false\n                showClearConfirmDialog = false\n            },\n            onDismiss = {\n                showClearConfirmDialog = false\n                scope.launch {\n                    runCatching { getFocusRequester(selectedIndex).requestFocus() }\n                }\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun DeleteHistoryConfirmDialog(\n    show: Boolean,\n    videoTitle: String,\n    onConfirm: () -> Unit,\n    onDismiss: () -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus()\n    }\n\n    TvAlertDialog(\n        onDismissRequest = onDismiss,\n        title = { Text(text = stringResource(R.string.history_delete_confirm_dialog_title)) },\n        text = {\n            Text(\n                text = stringResource(\n                    R.string.history_delete_confirm_dialog_text,\n                    videoTitle\n                )\n            )\n        },\n        confirmButton = {\n            Button(onClick = onConfirm) {\n                Text(text = stringResource(R.string.history_delete_confirm_dialog_confirm))\n            }\n        },\n        dismissButton = {\n            OutlinedButton(\n                modifier = Modifier.focusRequester(focusRequester),\n                onClick = onDismiss\n            ) {\n                Text(text = stringResource(R.string.history_delete_confirm_dialog_dismiss))\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun ClearHistoryConfirmDialog(\n    show: Boolean,\n    onConfirm: () -> Unit,\n    onDismiss: () -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n    var consumeInitialConfirmKeyUp by remember { mutableStateOf(false) }\n\n    fun handleInitialConfirmKeyUp(keyEvent: androidx.compose.ui.input.key.KeyEvent): Boolean {\n        if (!consumeInitialConfirmKeyUp) return false\n        val nativeKeyEvent = keyEvent.nativeKeyEvent\n        val isConfirmKey = nativeKeyEvent.keyCode == KeyEvent.KEYCODE_DPAD_CENTER ||\n            nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER ||\n            nativeKeyEvent.keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER\n        if (nativeKeyEvent.action == KeyEvent.ACTION_UP && isConfirmKey) {\n            consumeInitialConfirmKeyUp = false\n            return true\n        }\n        return false\n    }\n\n    LaunchedEffect(show) {\n        if (show) {\n            consumeInitialConfirmKeyUp = true\n            focusRequester.requestFocus()\n        }\n    }\n\n    TvAlertDialog(\n        onDismissRequest = onDismiss,\n        title = { Text(text = stringResource(R.string.history_clear_confirm_dialog_title)) },\n        text = {\n            Text(text = stringResource(R.string.history_clear_confirm_dialog_text))\n        },\n        confirmButton = {\n            Button(\n                modifier = Modifier.onPreviewKeyEvent { handleInitialConfirmKeyUp(it) },\n                onClick = onConfirm\n            ) {\n                Text(text = stringResource(R.string.history_delete_confirm_dialog_confirm))\n            }\n        },\n        dismissButton = {\n            OutlinedButton(\n                modifier = Modifier\n                    .focusRequester(focusRequester)\n                    .onPreviewKeyEvent { handleInitialConfirmKeyUp(it) },\n                onClick = onDismiss\n            ) {\n                Text(text = stringResource(R.string.history_delete_confirm_dialog_dismiss))\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/ToViewScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user\n\nimport android.view.KeyEvent\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.activities.video.UpInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.tv.util.stableItemKey\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.viewmodel.user.ToViewViewModel\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.koinInject\n\n@Composable\nfun ToViewScreen(\n    modifier: Modifier = Modifier,\n    toViewViewModel: ToViewViewModel = koinViewModel(),\n    showPageTitle: Boolean = true,\n    topTabFocusRequester: FocusRequester? = null\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val videoInfoRepository: VideoInfoRepository = koinInject()\n    val listFocusRestorer = rememberTvLazyListFocusRestorer()\n    val lazyGridState = rememberLazyGridState()\n    var currentIndex by remember { mutableIntStateOf(0) }\n    val showLargeTitle by remember { derivedStateOf { currentIndex < 4 } }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n\n    var deleteMode by remember { mutableStateOf(false) }\n    var showDeleteConfirmDialog by remember { mutableStateOf(false) }\n    var showClearConfirmDialog by remember { mutableStateOf(false) }\n    var selectedVideo by remember { mutableStateOf<VideoCardData?>(null) }\n    var selectedIndex by remember { mutableIntStateOf(0) }\n    var focusTopTabWhenListEmpty by remember { mutableStateOf(false) }\n\n    val focusRequesters = remember { mutableMapOf<Int, FocusRequester>() }\n    fun getFocusRequester(index: Int): FocusRequester {\n        return focusRequesters.getOrPut(index) { FocusRequester() }\n    }\n\n    LaunchedEffect(Unit) {\n        if (toViewViewModel.histories.isEmpty()) {\n            toViewViewModel.clearData()\n            toViewViewModel.update()\n        }\n    }\n\n    LaunchedEffect(toViewViewModel.deleting, toViewViewModel.histories.size, focusTopTabWhenListEmpty) {\n        if (!focusTopTabWhenListEmpty || toViewViewModel.deleting) return@LaunchedEffect\n        focusTopTabWhenListEmpty = false\n        if (toViewViewModel.histories.isEmpty()) {\n            deleteMode = false\n            topTabFocusRequester?.requestFocus(scope)\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            if (showPageTitle) {\n                Box(\n                    modifier = Modifier.padding(\n                        start = 48.dp,\n                        top = 24.dp,\n                        bottom = 8.dp,\n                        end = 48.dp\n                    )\n                ) {\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        verticalAlignment = Alignment.Bottom,\n                        horizontalArrangement = Arrangement.SpaceBetween\n                    ) {\n                        Text(\n                            text = stringResource(R.string.title_activity_toview),\n                            fontSize = titleFontSize.sp\n                        )\n                        if (toViewViewModel.noMore) {\n                            Text(\n                                text = stringResource(\n                                    R.string.load_data_count_no_more,\n                                    toViewViewModel.histories.size\n                                ),\n                                color = Color.White.copy(alpha = 0.6f)\n                            )\n                        } else {\n                            Text(\n                                text = stringResource(\n                                    R.string.load_data_count,\n                                    toViewViewModel.histories.size\n                                ),\n                                color = Color.White.copy(alpha = 0.6f)\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    ) { innerPadding ->\n        Column(modifier = Modifier.padding(innerPadding)) {\n            Text(\n                modifier = Modifier.fillMaxWidth().offset(x = (-20).dp, y = (-2).dp),\n                text = if (deleteMode) stringResource(R.string.delete_mode_action_hint) else stringResource(R.string.delete_mode_hint),\n                color = if (deleteMode) Color.Red.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),\n                fontSize = 11.sp,\n                textAlign = TextAlign.End\n            )\n            ProvideListBringIntoViewSpec(padding = 24.dp) {\n                LazyVerticalGrid(\n                    modifier = listFocusRestorer.containerModifier(\n                        Modifier.blockDownFocusExitAtGridEnd(\n                            currentIndex = currentIndex,\n                            itemCount = toViewViewModel.histories.size,\n                            columnCount = 4\n                        )\n                        .onPreviewKeyEvent { keyEvent ->\n                            if (keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_UP &&\n                                (keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_MENU ||\n                                 keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_DEL)\n                            ) {\n                                deleteMode = !deleteMode\n                                return@onPreviewKeyEvent true\n                            }\n                            if (deleteMode && keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_BACK) {\n                                if (keyEvent.nativeKeyEvent.action == KeyEvent.ACTION_UP) {\n                                    deleteMode = false\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                            false\n                        }\n                ),\n                columns = GridCells.Fixed(4),\n                state = lazyGridState,\n                contentPadding = PaddingValues(\n                    top = if (showPageTitle) 20.dp else 4.dp,\n                    bottom = 20.dp,\n                    start = 20.dp,\n                    end = 20.dp\n                ),\n                verticalArrangement = Arrangement.spacedBy(16.dp),\n                horizontalArrangement = Arrangement.spacedBy(13.dp)\n            ) {\n                itemsIndexed(\n                    items = toViewViewModel.histories,\n                    key = { _, item -> item.avid }\n                ) { index, item ->\n                    Box(\n                        contentAlignment = Alignment.Center\n                    ) {\n                        SmallVideoCard(\n                            modifier = listFocusRestorer.firstItemModifier(index)\n                                .focusRequester(getFocusRequester(index)),\n                            data = item,\n                            onClick = {\n                                if (deleteMode) {\n                                    selectedVideo = item\n                                    selectedIndex = index\n                                    showDeleteConfirmDialog = true\n                                } else {\n                                    videoInfoRepository.preloadedVideoList.clear()\n                                    videoInfoRepository.preloadedVideoList.addAll(toViewViewModel.histories)\n                                    VideoInfoActivity.actionStart(\n                                        context = context,\n                                        aid = item.avid,\n                                        proxyArea = ProxyArea.checkProxyArea(item.title)\n                                    )\n                                }\n                            },\n                            onLongClick = {\n                                if (deleteMode) {\n                                    selectedIndex = index\n                                    showClearConfirmDialog = true\n                                } else {\n                                    UpInfoActivity.actionStart(\n                                        context,\n                                        mid = item.upId,\n                                        name = item.upName,\n                                        face = item.upFace\n                                    )\n                                }\n                            },\n                            onFocus = {\n                                currentIndex = index\n                                //预加载\n                                // if (index + 12 > toViewViewModel.histories.size) {\n                                //     toViewViewModel.update()\n                                // }\n                            }\n                        )\n                    }\n                }\n\n                if (toViewViewModel.histories.isEmpty() && toViewViewModel.noMore) {\n                    item(span = { GridItemSpan(4) }) {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center\n                        ) {\n                            Text(\n                                text = stringResource(R.string.no_data),\n                                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                            )\n                        }\n                    }\n                }\n            }\n        }\n        }\n    }\n\n    if (showDeleteConfirmDialog && selectedVideo != null) {\n        DeleteToViewConfirmDialog(\n            show = showDeleteConfirmDialog,\n            videoTitle = selectedVideo!!.title,\n            onConfirm = {\n                if (topTabFocusRequester != null) {\n                    focusTopTabWhenListEmpty = true\n                }\n                val nextIndex = if (selectedIndex < toViewViewModel.histories.size - 1) selectedIndex + 1 else selectedIndex - 1\n                if (nextIndex >= 0) runCatching { getFocusRequester(nextIndex).requestFocus() }\n                toViewViewModel.deleteToView(\n                    avid = selectedVideo!!.avid\n                )\n                showDeleteConfirmDialog = false\n                selectedVideo = null\n            },\n            onDismiss = {\n                showDeleteConfirmDialog = false\n                scope.launch {\n                    runCatching { getFocusRequester(selectedIndex).requestFocus() }\n                }\n                selectedVideo = null\n            }\n        )\n    }\n\n    if (showClearConfirmDialog) {\n        ClearToViewConfirmDialog(\n            show = showClearConfirmDialog,\n            onConfirm = {\n                if (topTabFocusRequester != null) {\n                    focusTopTabWhenListEmpty = true\n                }\n                toViewViewModel.clearToView()\n                deleteMode = false\n                showClearConfirmDialog = false\n            },\n            onDismiss = {\n                showClearConfirmDialog = false\n                scope.launch {\n                    runCatching { getFocusRequester(selectedIndex).requestFocus() }\n                }\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun DeleteToViewConfirmDialog(\n    show: Boolean,\n    videoTitle: String,\n    onConfirm: () -> Unit,\n    onDismiss: () -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus()\n    }\n\n    TvAlertDialog(\n        onDismissRequest = onDismiss,\n        title = { Text(text = stringResource(R.string.toview_delete_confirm_dialog_title)) },\n        text = {\n            Text(\n                text = stringResource(\n                    R.string.toview_delete_confirm_dialog_text,\n                    videoTitle\n                )\n            )\n        },\n        confirmButton = {\n            Button(onClick = onConfirm) {\n                Text(text = stringResource(R.string.toview_delete_confirm_dialog_confirm))\n            }\n        },\n        dismissButton = {\n            OutlinedButton(\n                modifier = Modifier.focusRequester(focusRequester),\n                onClick = onDismiss\n            ) {\n                Text(text = stringResource(R.string.toview_delete_confirm_dialog_dismiss))\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun ClearToViewConfirmDialog(\n    show: Boolean,\n    onConfirm: () -> Unit,\n    onDismiss: () -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n    var consumeInitialConfirmKeyUp by remember { mutableStateOf(false) }\n\n    fun handleInitialConfirmKeyUp(keyEvent: androidx.compose.ui.input.key.KeyEvent): Boolean {\n        if (!consumeInitialConfirmKeyUp) return false\n        val nativeKeyEvent = keyEvent.nativeKeyEvent\n        val isConfirmKey = nativeKeyEvent.keyCode == KeyEvent.KEYCODE_DPAD_CENTER ||\n            nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER ||\n            nativeKeyEvent.keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER\n        if (nativeKeyEvent.action == KeyEvent.ACTION_UP && isConfirmKey) {\n            consumeInitialConfirmKeyUp = false\n            return true\n        }\n        return false\n    }\n\n    LaunchedEffect(show) {\n        if (show) {\n            consumeInitialConfirmKeyUp = true\n            focusRequester.requestFocus()\n        }\n    }\n\n    TvAlertDialog(\n        onDismissRequest = onDismiss,\n        title = { Text(text = stringResource(R.string.toview_clear_confirm_dialog_title)) },\n        text = {\n            Text(text = stringResource(R.string.toview_clear_confirm_dialog_text))\n        },\n        confirmButton = {\n            Button(\n                modifier = Modifier.onPreviewKeyEvent { handleInitialConfirmKeyUp(it) },\n                onClick = onConfirm\n            ) {\n                Text(text = stringResource(R.string.toview_delete_confirm_dialog_confirm))\n            }\n        },\n        dismissButton = {\n            OutlinedButton(\n                modifier = Modifier\n                    .focusRequester(focusRequester)\n                    .onPreviewKeyEvent { handleInitialConfirmKeyUp(it) },\n                onClick = onDismiss\n            ) {\n                Text(text = stringResource(R.string.toview_delete_confirm_dialog_dismiss))\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/UpInfoScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user\n\nimport android.app.Activity\nimport android.content.res.Configuration\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Add\nimport androidx.compose.material.icons.rounded.Done\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Border\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.ExperimentalTvMaterial3Api\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.repositories.UserRepository\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.component.videocard.SmallVideoCard\nimport dev.aaa1115910.bv.tv.manager.FollowStateManager\nimport dev.aaa1115910.bv.tv.util.blockDownFocusExitAtGridEnd\nimport dev.aaa1115910.bv.tv.util.ProvideListBringIntoViewSpec\nimport dev.aaa1115910.bv.tv.util.rememberTvLazyListFocusRestorer\nimport dev.aaa1115910.bv.tv.util.stableItemKey\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.user.UserSpaceViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport dev.aaa1115910.bv.repository.VideoInfoRepository\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.getKoin\nimport org.koin.compose.koinInject\n\n@OptIn(ExperimentalTvMaterial3Api::class)\n@Composable\nfun UpSpaceScreen(\n    modifier: Modifier = Modifier,\n    userSpaceViewModel: UserSpaceViewModel = koinViewModel(),\n    userRepository: UserRepository = getKoin().get(),\n) {\n    val context = LocalContext.current\n    val videoInfoRepository: VideoInfoRepository = koinInject()\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger { }\n    var currentIndex by remember { mutableIntStateOf(0) }\n    val showLargeTitle by remember { derivedStateOf { currentIndex < 4 } }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 40f else 24f,\n        label = \"title font size\"\n    )\n    val infoFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 15f else 12f,\n        label = \"info font size\"\n    )\n\n    var showFollowButton by remember { mutableStateOf(false) }\n    var isFollowing by remember { mutableStateOf(false) }\n    var isLongPress by remember { mutableStateOf(false) }\n\n    // 监听关注状态变化\n    val followStateMap by FollowStateManager.followStateMap.collectAsState()\n\n    // 当关注状态map变化时，更新当前用户的关注状态\n    LaunchedEffect(followStateMap, userSpaceViewModel.upMid) {\n        if (userSpaceViewModel.upMid > 0) {\n            FollowStateManager.getFollowState(userSpaceViewModel.upMid)?.let { following ->\n                isFollowing = following\n            }\n        }\n    }\n\n    val listFocusRequester = remember { FocusRequester() }\n    val listFocusRestorer = rememberTvLazyListFocusRestorer(listFocusRequester)\n    LaunchedEffect(userSpaceViewModel.tvSpaceVideos.isNotEmpty()) {\n        listFocusRequester.requestFocus()\n    }\n\n    val addFollow: (afterModify: (success: Boolean) -> Unit) -> Unit = { afterModify ->\n        scope.launch(Dispatchers.IO) {\n            val userMid = userSpaceViewModel.upMid\n            logger.fInfo { \"Add follow to user $userMid\" }\n            val success = userRepository.followUser(\n                mid = userMid,\n                preferApiType = Prefs.apiType\n            )\n            logger.fInfo { \"Add follow result: $success\" }\n            // 更新缓存状态\n            if (success) {\n                FollowStateManager.updateFollowState(userMid, true)\n            }\n            afterModify(success)\n        }\n    }\n\n    val delFollow: (afterModify: (success: Boolean) -> Unit) -> Unit = { afterModify ->\n        scope.launch(Dispatchers.IO) {\n            val userMid = userSpaceViewModel.upMid\n            logger.fInfo { \"Del follow to user $userMid\" }\n            val success = userRepository.unfollowUser(\n                mid = userMid,\n                preferApiType = Prefs.apiType\n            )\n            logger.fInfo { \"Del follow result: $success\" }\n            // 更新缓存状态\n            if (success) {\n                FollowStateManager.updateFollowState(userMid, false)\n            }\n            afterModify(success)\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        val intent = (context as Activity).intent\n        if (intent.hasExtra(\"mid\")) {\n            val mid = intent.getLongExtra(\"mid\", 0)\n            val name = intent.getStringExtra(\"name\") ?: \"\"\n            val face = intent.getStringExtra(\"face\") ?: \"\"\n            userSpaceViewModel.upMid = mid\n            userSpaceViewModel.upName = name\n            userSpaceViewModel.upFace = face\n            scope.launch(Dispatchers.IO) {\n                runCatching {\n                    val userInfo = userRepository.getUserCardInfo(\n                        mid = mid\n                    )\n                    withContext(Dispatchers.Main) {\n                        userSpaceViewModel.upName = userInfo.card.name\n                        userSpaceViewModel.upFace = userInfo.card.face\n                        userSpaceViewModel.sign = userInfo.card.sign.replace(\"\\n\", \"\")\n                        userSpaceViewModel.fans = userInfo.card.fans\n                        userSpaceViewModel.friend = userInfo.card.attention\n\n                        if (Prefs.isLogin) {\n                            FollowStateManager.updateFollowState(mid, userInfo.following)\n                            isFollowing = userInfo.following\n                            showFollowButton = true\n                        }\n                    }\n                }.onFailure {\n                    logger.fInfo { \"Get user info failed: ${it.stackTraceToString()}\" }\n                }\n            }\n            userSpaceViewModel.update()\n        } else {\n            context.finish()\n        }\n    }\n\n    Scaffold(\n        modifier = modifier\n            .onPreviewKeyEvent {\n                val isDpadCenter =\n                    listOf(Key.Enter, Key.DirectionCenter).contains(it.key)\n\n                if (isDpadCenter && it.type == KeyEventType.KeyDown) {\n                    isLongPress = it.nativeKeyEvent.repeatCount > 0\n                }\n                false\n            },\n        topBar = {\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(start = 24.dp, top = 12.dp, bottom = 12.dp, end = 24.dp),\n                verticalAlignment = Alignment.Bottom,\n                horizontalArrangement = Arrangement.spacedBy(24.dp)\n            ) {\n                Column(\n                    modifier = Modifier.weight(1f)\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        if (userSpaceViewModel.upFace.isNotBlank()) {\n                            val imageUrl = userSpaceViewModel.upFace + \"@128w_128h_1c_1s.webp\"\n                            AsyncImage(\n                                modifier = Modifier\n                                    .size(titleFontSize.dp)\n                                    .clip(CircleShape),\n                                model = imageUrl,\n                                contentDescription = null,\n                                contentScale = ContentScale.Crop,\n                                onError = { error ->\n                                    userSpaceViewModel.upFace = \"\"\n\n                                    println(\"Failed to load avatar: $imageUrl\")\n                                    println(\"Error message: ${error.result.throwable}\")\n                                },\n                                onSuccess = { println(\"Avatar loaded successfully: $imageUrl\") }\n                            )\n                            Spacer(modifier = Modifier.width(8.dp))\n                        }\n                        Text(\n                            text = userSpaceViewModel.upName,\n                            fontSize = titleFontSize.sp\n                        )\n                        // 关注按钮\n                        if (showFollowButton) {\n                            Surface(\n                                modifier = Modifier\n                                    .padding(start = if (showLargeTitle) 24.dp else 4.dp, top = 2.dp)\n                                    .scale(if (showLargeTitle) 1f else 0.7f),\n                                onClick = {\n                                    if (isFollowing) {\n                                        delFollow { success ->\n                                            scope.launch(Dispatchers.Main) {\n                                                if (success) {\n                                                    \"已取消关注\".toast(context)\n                                                } else {\n                                                    \"取消关注失败\".toast(context)\n                                                }\n                                            }\n                                        }\n                                    } else {\n                                        addFollow { success ->\n                                            scope.launch(Dispatchers.Main) {\n                                                if (success) {\n                                                    \"关注成功\".toast(context)\n                                                } else {\n                                                    \"关注失败\".toast(context)\n                                                }\n                                            }\n                                        }\n                                    }\n                                },\n                                colors = ClickableSurfaceDefaults.colors(\n                                    containerColor = MaterialTheme.colorScheme.onSurface.copy(\n                                        alpha = 0.2f\n                                    ),\n                                    focusedContainerColor = MaterialTheme.colorScheme.onSurface.copy(\n                                        alpha = 0.2f\n                                    ),\n                                    pressedContainerColor = MaterialTheme.colorScheme.onSurface.copy(\n                                        alpha = 0.3f\n                                    )\n                                ),\n                                shape = ClickableSurfaceDefaults.shape(\n                                    shape = MaterialTheme.shapes.small\n                                ),\n                                border = ClickableSurfaceDefaults.border(\n                                    focusedBorder = Border(\n                                        border = BorderStroke(\n                                            width = 2.dp,\n                                            color = MaterialTheme.colorScheme.onSurface.copy(\n                                                alpha = 0.3f\n                                            )\n                                        ),\n                                        shape = MaterialTheme.shapes.small\n                                    )\n                                )\n                            ) {\n                                Row(\n                                    modifier = Modifier.padding(\n                                        horizontal = 10.dp,\n                                        vertical = 5.dp\n                                    ),\n                                    verticalAlignment = Alignment.CenterVertically,\n                                    horizontalArrangement = Arrangement.spacedBy(4.dp)\n                                ) {\n                                    if (isFollowing) {\n                                        Icon(\n                                            imageVector = Icons.Rounded.Done,\n                                            contentDescription = null,\n                                            tint = MaterialTheme.colorScheme.onSurface,\n                                            modifier = Modifier.size(18.dp)\n                                        )\n                                        Text(\n                                            text = stringResource(R.string.video_info_followed),\n                                            color = MaterialTheme.colorScheme.onSurface,\n                                            fontSize = 15.sp\n                                        )\n                                    } else {\n                                        Icon(\n                                            imageVector = Icons.Rounded.Add,\n                                            contentDescription = null,\n                                            tint = MaterialTheme.colorScheme.onSurface,\n                                            modifier = Modifier.size(18.dp)\n                                        )\n                                        Text(\n                                            text = stringResource(R.string.video_info_follow),\n                                            color = MaterialTheme.colorScheme.onSurface,\n                                            fontSize = 15.sp\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    Row {\n                        Text(\n                            modifier = Modifier.padding(top =  if (showLargeTitle) 6.dp else 4.dp),\n                            text = stringResource(\n                                R.string.friend_count,\n                                if (userSpaceViewModel.friend >= 10000) String.format(\n                                    \"%.2f\",\n                                    userSpaceViewModel.friend / 10000.0\n                                ) + \" 万\" else userSpaceViewModel.friend.toString()\n                            ) + \" · \" + stringResource(\n                                R.string.fans_count,\n                                if (userSpaceViewModel.fans >= 10000) String.format(\n                                    \"%.2f\",\n                                    userSpaceViewModel.fans / 10000.0\n                                ) + \" 万\" else userSpaceViewModel.fans.toString()\n                            ) + \"${if (userSpaceViewModel.sign.isNotEmpty()) \"   ｜   \" + userSpaceViewModel.sign else \"\"}\",\n                            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),\n                            fontSize = infoFontSize.sp,\n                            maxLines = 2,\n                            overflow = TextOverflow.Ellipsis,\n                            lineHeight = (infoFontSize * 1.4).sp\n                        )\n                    }\n                }\n                Column {\n                    Text(\n                        text = stringResource(\n                            R.string.load_data_count,\n                            userSpaceViewModel.tvSpaceVideos.size\n                        ),\n                        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                    )\n                    AnimatedVisibility(visible = userSpaceViewModel.noMore) {\n                        Text(\n                            text = stringResource(R.string.load_data_no_more),\n                            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)\n                        )\n                    }\n                }\n            }\n        }\n    ) { innerPadding ->\n        ProvideListBringIntoViewSpec(padding = 26.dp) {\n            LazyVerticalGrid(\n                modifier = listFocusRestorer.containerModifier(\n                    Modifier\n                        .padding(innerPadding)\n                        .blockDownFocusExitAtGridEnd(\n                            currentIndex = currentIndex,\n                            itemCount = userSpaceViewModel.tvSpaceVideos.size,\n                            columnCount = 4\n                        )\n                ),\n                columns = GridCells.Fixed(4),\n                contentPadding = PaddingValues(24.dp),\n                verticalArrangement = Arrangement.spacedBy(24.dp),\n                horizontalArrangement = Arrangement.spacedBy(24.dp)\n            ) {\n                itemsIndexed(\n                    items = userSpaceViewModel.tvSpaceVideos,\n                    key = { index, video -> \"$index-${video.stableItemKey()}\" }\n                ) { index, video ->\n                    SmallVideoCard(\n                        modifier = listFocusRestorer.firstItemModifier(index),\n                        data = video,\n                        onClick = {\n                            if (!isLongPress) {\n                                videoInfoRepository.preloadedVideoList.clear()\n                                videoInfoRepository.preloadedVideoList.addAll(userSpaceViewModel.tvSpaceVideos)\n                                VideoInfoActivity.actionStart(\n                                    context = context,\n                                    aid = video.avid,\n                                    proxyArea = ProxyArea.checkProxyArea(video.title)\n                                )\n                            }\n                        },\n                        onFocus = {\n                            currentIndex = index\n                            if (index + 12 > userSpaceViewModel.tvSpaceVideos.size) {\n                                userSpaceViewModel.update()\n                            }\n                        }\n                    )\n                }\n            }\n        }\n    }\n}\n\n//https://i2.hdslb.com/bfs/face/ea9b2fd60b04b123d0b48477838f60532b6271cd.jpg\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nprivate fun UpFacePreview() {\n    BVTheme {\n        AsyncImage(\n            modifier = Modifier\n                .size(80.dp)\n                .clip(CircleShape),\n            model = \"https://i2.hdslb.com/bfs/face/ea9b2fd60b04b123d0b48477838f60532b6271cd.jpg@80h_80w_1c.webp\",\n            contentDescription = null,\n            contentScale = ContentScale.Crop\n        )\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/UserInfoScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user\n\nimport android.app.Activity\nimport android.content.Intent\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.animateIntAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.ButtonDefaults\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonStatus\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonType\nimport dev.aaa1115910.biliapi.http.entity.AuthFailureException\nimport dev.aaa1115910.biliapi.repositories.FavoriteRepository\nimport dev.aaa1115910.biliapi.repositories.HistoryRepository\nimport dev.aaa1115910.biliapi.repositories.SeasonRepository\nimport dev.aaa1115910.biliapi.repositories.UserRepository\nimport dev.aaa1115910.bv.BuildConfig\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.carddata.SeasonCardData\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.activities.user.FavoriteActivity\nimport dev.aaa1115910.bv.tv.activities.user.FollowActivity\nimport dev.aaa1115910.bv.tv.activities.user.FollowingSeasonActivity\nimport dev.aaa1115910.bv.tv.activities.user.HistoryActivity\nimport dev.aaa1115910.bv.tv.activities.user.UserSwitchActivity\nimport dev.aaa1115910.bv.tv.activities.video.SeasonInfoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoInfoActivity\nimport dev.aaa1115910.bv.tv.component.videocard.SeasonCard\nimport dev.aaa1115910.bv.tv.component.videocard.VideosRow\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.Prefs\nimport dev.aaa1115910.bv.util.fException\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.fWarn\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.UserViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.getKoin\n\n@Composable\nfun UserInfoScreen(\n    modifier: Modifier = Modifier,\n    userViewModel: UserViewModel = koinViewModel(),\n    userRepository: UserRepository = getKoin().get(),\n    favoriteRepository: FavoriteRepository = getKoin().get(),\n    seasonRepository: SeasonRepository = getKoin().get(),\n    historyRepository: HistoryRepository = getKoin().get(),\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val lifecycleOwner = LocalLifecycleOwner.current\n    val logger = KotlinLogging.logger { }\n    val focusRequester = remember { FocusRequester() }\n    var showLargeTitle by remember { mutableStateOf(true) }\n\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (showLargeTitle) 48f else 24f,\n        label = \"title font size\"\n    )\n    val randomTitleList = context.resources.getStringArray(R.array.user_homepage_random_title)\n    val title by remember { mutableStateOf(randomTitleList.random()) }\n\n    var followingUpCount by remember { mutableIntStateOf(0) }\n\n    val histories = remember { mutableStateListOf<VideoCardData>() }\n    val animes = remember { mutableStateListOf<SeasonCardData>() }\n    val favorites = remember { mutableStateListOf<VideoCardData>() }\n\n    val updateHistories = {\n        scope.launch(Dispatchers.IO) {\n            runCatching {\n                val data = historyRepository.getHistories(\n                    cursor = 0,\n                    preferApiType = Prefs.apiType\n                )\n                histories.clear()\n                data.data.forEach { historyItem ->\n                    histories.add(\n                        VideoCardData(\n                            avid = historyItem.oid,\n                            title = historyItem.title,\n                            cover = historyItem.cover,\n                            upName = historyItem.author,\n                            timeString = if (historyItem.progress == -1) context.getString(R.string.play_time_finish)\n                            else context.getString(\n                                R.string.play_time_history,\n                                (historyItem.progress * 1000L).formatHourMinSec(),\n                                (historyItem.duration * 1000L).formatHourMinSec()\n                            )\n                        )\n                    )\n                }\n            }.onFailure {\n                logger.fWarn { \"Load recent videos failed: ${it.stackTraceToString()}\" }\n                when (it) {\n                    is AuthFailureException -> {\n                        withContext(Dispatchers.Main) {\n                            context.getString(R.string.exception_auth_failure).toast(context)\n                        }\n                        logger.fInfo { \"User auth failure\" }\n                        if (!BuildConfig.DEBUG) userViewModel.logout()\n                    }\n\n                    else -> {}\n                }\n            }\n        }\n    }\n\n    val updateFollowedAnimes = {\n        scope.launch(Dispatchers.IO) {\n            runCatching {\n                val followingSeasonData = seasonRepository.getFollowingSeasons(\n                    type = FollowingSeasonType.Bangumi,\n                    status = FollowingSeasonStatus.All,\n                    pageNumber = 1,\n                    pageSize = 15,\n                    preferApiType = Prefs.apiType\n                )\n                animes.clear()\n                followingSeasonData.list.forEach { followedSeason ->\n                    animes.add(\n                        SeasonCardData(\n                            seasonId = followedSeason.seasonId,\n                            title = followedSeason.title,\n                            cover = followedSeason.cover,\n                            rating = null\n                        )\n                    )\n                }\n            }.onFailure {\n                logger.fWarn { \"Load followed animes failed: ${it.stackTraceToString()}\" }\n                when (it) {\n                    is AuthFailureException -> {\n                        withContext(Dispatchers.Main) {\n                            context.getString(R.string.exception_auth_failure).toast(context)\n                        }\n                        logger.fInfo { \"User auth failure\" }\n                        if (!BuildConfig.DEBUG) userViewModel.logout()\n                    }\n\n                    else -> {}\n                }\n            }\n        }\n    }\n\n    val updateFavoriteVideos = {\n        scope.launch(Dispatchers.IO) {\n            var defaultFolderId: Long = 0\n            runCatching {\n                val favoriteFolderMetadataList =\n                    favoriteRepository.getAllFavoriteFolderMetadataList(\n                        mid = Prefs.uid,\n                        preferApiType = Prefs.apiType\n                    )\n                if (favoriteFolderMetadataList.isEmpty()) {\n                    \"未找到收藏夹\".toast(context)\n                    return@launch\n                }\n                defaultFolderId =\n                    favoriteFolderMetadataList.find { it.title == \"默认收藏夹\" }?.id ?: 0\n                logger.fInfo { \"Get favorite folders: ${favoriteFolderMetadataList.map { it.id }}\" }\n            }.onFailure {\n                logger.fException(it) { \"Load favorite folders failed\" }\n            }\n            runCatching {\n                val favoriteItems = favoriteRepository.getFavoriteFolderData(\n                    mediaId = defaultFolderId,\n                    preferApiType = Prefs.apiType\n                ).medias\n                favorites.clear()\n                favoriteItems.forEach { favoriteItem ->\n                    favorites.add(\n                        VideoCardData(\n                            avid = favoriteItem.id,\n                            title = favoriteItem.title,\n                            cover = favoriteItem.cover,\n                            upName = favoriteItem.upper.name,\n                            time = favoriteItem.duration.toLong() * 1000\n                        )\n                    )\n                }\n            }.onFailure {\n                logger.fWarn { \"Load favorite items failed: ${it.stackTraceToString()}\" }\n                when (it) {\n                    is AuthFailureException -> {\n                        withContext(Dispatchers.Main) {\n                            context.getString(R.string.exception_auth_failure).toast(context)\n                        }\n                        logger.fInfo { \"User auth failure\" }\n                        if (!BuildConfig.DEBUG) userViewModel.logout()\n                    }\n\n                    else -> {}\n                }\n            }\n        }\n    }\n\n    val updateFollowingUpCount = {\n        scope.launch(Dispatchers.IO) {\n            logger.fInfo { \"Update following up count with user ${Prefs.uid}\" }\n            followingUpCount = userRepository.getFollowingUpCount(\n                mid = Prefs.uid,\n                preferApiType = Prefs.apiType\n            )\n            logger.fInfo { \"Following up count: $followingUpCount\" }\n        }\n    }\n    val updateData = {\n        if (!userViewModel.isLogin) {\n            (context as? Activity)?.finish()\n        } else {\n            userViewModel.updateUserInfo(forceUpdate = true)\n            updateHistories()\n            updateFollowedAnimes()\n            updateFavoriteVideos()\n            updateFollowingUpCount()\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus()\n        updateData()\n    }\n\n    DisposableEffect(lifecycleOwner) {\n        var leaveFromThisPage = false\n        val observer = LifecycleEventObserver { _, event ->\n            if (event == Lifecycle.Event.ON_PAUSE) {\n                leaveFromThisPage = true\n            } else if (event == Lifecycle.Event.ON_RESUME) {\n                if (leaveFromThisPage) updateData()\n                leaveFromThisPage = false\n            }\n        }\n\n        lifecycleOwner.lifecycle.addObserver(observer)\n\n        onDispose {\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            Box(\n                modifier = Modifier.padding(start = 48.dp, top = 24.dp, bottom = 8.dp)\n            ) {\n                Text(\n                    text = title,\n                    fontSize = titleFontSize.sp\n                )\n            }\n\n        }\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier.padding(innerPadding),\n            contentPadding = PaddingValues(bottom = 24.dp)\n        ) {\n            item {\n                UserRow(\n                    modifier = Modifier\n                        .focusRequester(focusRequester)\n                        .onFocusChanged {\n                            showLargeTitle = it.hasFocus\n                        },\n                    username = userViewModel.username,\n                    face = userViewModel.face,\n                    uid = userViewModel.responseData?.mid ?: 0,\n                    level = userViewModel.responseData?.level ?: 0,\n                    currentExp = userViewModel.responseData?.levelExp?.currentExp ?: 0,\n                    nextLevelExp = with(userViewModel.responseData?.levelExp?.nextExp) {\n                        if (this == null) {\n                            1\n                        } else if (this <= 0) {\n                            userViewModel.responseData?.levelExp?.currentExp ?: 1\n                        } else {\n                            (userViewModel.responseData?.levelExp?.currentExp ?: 1)\n                            +(userViewModel.responseData?.levelExp?.nextExp ?: 0)\n                        }\n                    },\n                    showLabel = userViewModel.responseData?.vip?.avatarSubscript == 1,\n                    labelUrl = userViewModel.responseData?.vip?.label?.imgLabelUriHansStatic ?: \"\",\n                    followingUpCount = followingUpCount,\n                    onOpenFollowingUser = {\n                        context.startActivity(Intent(context, FollowActivity::class.java))\n                    },\n                    onOpenUserSwitch = {\n                        context.startActivity(Intent(context, UserSwitchActivity::class.java))\n                    },\n                    coins = userViewModel.responseData?.coins ?: 0f\n                )\n            }\n            item {\n                RecentVideosRow(\n                    videos = histories,\n                    showMore = {\n                        context.startActivity(Intent(context, HistoryActivity::class.java))\n                    }\n                )\n            }\n            item {\n                FollowingAnimeVideosRow(\n                    videos = animes,\n                    showMore = {\n                        context.startActivity(Intent(context, FollowingSeasonActivity::class.java))\n                    }\n                )\n            }\n            item {\n                FavoriteVideosRow(\n                    videos = favorites,\n                    showMore = {\n                        context.startActivity(Intent(context, FavoriteActivity::class.java))\n                    }\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun UserInfo(\n    modifier: Modifier = Modifier,\n    face: String,\n    username: String,\n    uid: Long,\n    level: Int,\n    currentExp: Int,\n    nextLevelExp: Int,\n    showLabel: Boolean,\n    labelUrl: String,\n    onClick: () -> Unit,\n    coins: Float = 0f\n) {\n    var hasFocus by remember { mutableStateOf(false) }\n    val levelSlider by animateFloatAsState(\n        targetValue = currentExp.toFloat() / nextLevelExp,\n        animationSpec = tween(\n            durationMillis = 1500,\n            easing = LinearEasing\n        ),\n        label = \"Loading level exp slider\"\n    )\n\n    Surface(\n        modifier = modifier\n            .size(480.dp, 140.dp)\n            .onFocusChanged { hasFocus = it.hasFocus },\n        colors = ClickableSurfaceDefaults.colors(containerColor = MaterialTheme.colorScheme.secondaryContainer),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        onClick = onClick\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            androidx.compose.material3.Surface(\n                modifier = Modifier\n                    .padding(start = 24.dp, end = 8.dp)\n                    .size(80.dp)\n                    .clip(CircleShape),\n                color = Color.White\n            ) {\n                AsyncImage(\n                    modifier = Modifier\n                        .size(80.dp)\n                        .clip(CircleShape),\n                    model = face,\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n            }\n            Column(\n                modifier = Modifier\n                    .padding(\n                        start = 6.dp,\n                        top = 24.dp,\n                        end = 24.dp,\n                        bottom = 24.dp\n                    )\n                    .height(80.dp),\n                verticalArrangement = Arrangement.SpaceBetween\n            ) {\n                val startPaddingValue = 6.dp\n                CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {\n                    Row(\n                        modifier = Modifier.padding(end = startPaddingValue),\n                        horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        if (showLabel)\n                            AsyncImage(\n                                //大会员 Tag 给定指定大小范围，避免加载时大小会突然变得非常大导致画面闪烁\n                                modifier = Modifier\n                                    .height(22.dp)\n                                    .widthIn(max = 96.dp),\n                                model = labelUrl,\n                                contentDescription = null,\n                                contentScale = ContentScale.FillHeight\n                            )\n                        Text(\n                            text = username,\n                            style = MaterialTheme.typography.titleLarge,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                }\n\n                Row(\n                    modifier = Modifier.padding(start = startPaddingValue),\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    verticalAlignment = Alignment.Bottom\n                ) {\n                    Text(text = stringResource(R.string.user_info_level, level))\n                    Text(text = stringResource(R.string.user_info_coins, coins))\n                    Text(text = stringResource(R.string.user_info_uid, uid))\n                }\n\n                val sliderColor = if (hasFocus) {\n                    SliderDefaults.colors(\n                        disabledThumbColor = Color.Transparent,\n                        disabledActiveTrackColor = MaterialTheme.colorScheme.inverseOnSurface,\n                        disabledInactiveTrackColor = MaterialTheme.colorScheme.inverseOnSurface.copy(\n                            alpha = 0.3f\n                        )\n                    )\n                } else {\n                    SliderDefaults.colors(\n                        disabledThumbColor = Color.Transparent,\n                        disabledActiveTrackColor = MaterialTheme.colorScheme.primary,\n                    )\n                }\n\n                LinearProgressIndicator(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(8.dp),\n                    progress = { levelSlider },\n                    gapSize = 0.dp,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun IncognitoModeCard(\n    modifier: Modifier = Modifier\n) {\n    var enabled by remember { mutableStateOf(false) }\n\n    LaunchedEffect(Unit) {\n        enabled = Prefs.incognitoMode\n    }\n\n    IncognitoModeCardContent(\n        modifier = modifier,\n        enabled = enabled,\n        onClick = {\n            enabled = !enabled\n            Prefs.incognitoMode = enabled\n        }\n    )\n}\n\n@Composable\nprivate fun IncognitoModeCardContent(\n    modifier: Modifier = Modifier,\n    enabled: Boolean,\n    onClick: () -> Unit\n) {\n    Surface(\n        modifier = modifier.height(140.dp),\n        colors = ClickableSurfaceDefaults.colors(containerColor = MaterialTheme.colorScheme.secondaryContainer),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        onClick = onClick\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxHeight()\n                .padding(horizontal = 24.dp, vertical = 24.dp),\n            verticalArrangement = Arrangement.SpaceAround,\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Text(\n                text = stringResource(R.string.user_info_Incognito_mode_title),\n                style = MaterialTheme.typography.titleLarge\n            )\n            Text(\n                text = if (enabled) \"\\uD83D\\uDC7B\" + stringResource(R.string.user_info_Incognito_mode_on)\n                else stringResource(R.string.user_info_Incognito_mode_off),\n                style = MaterialTheme.typography.titleMedium\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun FollowedUserCard(\n    modifier: Modifier = Modifier,\n    size: Int,\n    onClick: () -> Unit\n) {\n    Surface(\n        modifier = modifier.height(140.dp),\n        colors = ClickableSurfaceDefaults.colors(containerColor = MaterialTheme.colorScheme.secondaryContainer),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        onClick = onClick\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxHeight()\n                .padding(horizontal = 24.dp, vertical = 24.dp),\n            verticalArrangement = Arrangement.SpaceAround,\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Text(\n                text = stringResource(R.string.user_homepage_follow),\n                style = MaterialTheme.typography.titleLarge\n            )\n            Text(\n                text = \"$size\",\n                style = MaterialTheme.typography.titleMedium\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun UserSwitchCard(\n    modifier: Modifier = Modifier,\n    onClick: () -> Unit\n) {\n    Surface(\n        modifier = modifier.height(140.dp),\n        colors = ClickableSurfaceDefaults.colors(containerColor = MaterialTheme.colorScheme.secondaryContainer),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),\n        onClick = onClick\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxHeight()\n                .padding(horizontal = 24.dp, vertical = 24.dp),\n            verticalArrangement = Arrangement.SpaceAround,\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Text(\n                text = stringResource(R.string.user_homepage_user_switch),\n                style = MaterialTheme.typography.titleLarge\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun UserRow(\n    modifier: Modifier = Modifier,\n    username: String,\n    face: String,\n    uid: Long,\n    level: Int,\n    currentExp: Int,\n    nextLevelExp: Int,\n    showLabel: Boolean,\n    labelUrl: String,\n    followingUpCount: Int,\n    onOpenFollowingUser: () -> Unit,\n    onOpenUserSwitch: () -> Unit,\n    coins: Float = 0f\n) {\n    val animateFollowingNumber by animateIntAsState(\n        targetValue = followingUpCount,\n        label = \"animate following number\"\n    )\n\n    LazyRow(\n        modifier = modifier.padding(vertical = 28.dp),\n        horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),\n        contentPadding = PaddingValues(horizontal = 50.dp)\n    ) {\n        item {\n            UserInfo(\n                modifier = Modifier,\n                face = face,\n                username = username,\n                uid = uid,\n                level = level,\n                currentExp = currentExp,\n                nextLevelExp = nextLevelExp,\n                showLabel = showLabel,\n                labelUrl = labelUrl,\n                onClick = { },\n                coins = coins\n            )\n        }\n        item {\n            IncognitoModeCard()\n        }\n        item {\n            FollowedUserCard(\n                size = animateFollowingNumber,\n                onClick = onOpenFollowingUser\n            )\n        }\n        item {\n            UserSwitchCard(\n                onClick = onOpenUserSwitch\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun RecentVideosRow(\n    modifier: Modifier = Modifier,\n    videos: List<VideoCardData>,\n    showMore: () -> Unit\n) {\n    val context = LocalContext.current\n    VideosRow(\n        modifier = modifier\n            .padding(vertical = 8.dp),\n        header = stringResource(R.string.user_homepage_recent),\n        hideShowMore = false,\n        showMore = showMore,\n        videos = videos,\n        onOpenSeasonInfo = { videoData ->\n            SeasonInfoActivity.actionStart(\n                context = context,\n                epId = videoData.epId!!,\n                proxyArea = ProxyArea.checkProxyArea(videoData.title)\n            )\n        },\n        onOpenVideoInfo = { videoData ->\n            VideoInfoActivity.actionStart(context, videoData.avid)\n        }\n    )\n}\n\n@Composable\nprivate fun FollowingAnimeVideosRow(\n    modifier: Modifier = Modifier,\n    videos: List<SeasonCardData>,\n    showMore: () -> Unit\n) {\n    val context = LocalContext.current\n    val density = LocalDensity.current\n    var hasFocus by remember { mutableStateOf(false) }\n    val titleFontSize by animateFloatAsState(\n        targetValue = if (hasFocus) 30f else 14f,\n        label = \"title font size\",\n        animationSpec = tween(\n            durationMillis = 120\n        )\n    )\n    var rowHeight by remember { mutableStateOf(0.dp) }\n\n    Column(\n        modifier = modifier\n            .padding(vertical = 8.dp)\n            .onFocusChanged { hasFocus = it.hasFocus }\n    ) {\n        Text(\n            modifier = Modifier.padding(start = 50.dp),\n            text = stringResource(R.string.user_homepage_anime),\n            fontSize = titleFontSize.sp\n        )\n        LazyRow(\n            modifier = Modifier\n                .padding(top = 15.dp)\n                .onGloballyPositioned {\n                    rowHeight = with(density) {\n                        it.size.height.toDp()\n                    }\n                },\n            horizontalArrangement = Arrangement.spacedBy(24.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            contentPadding = PaddingValues(horizontal = 62.dp)\n        ) {\n            itemsIndexed(\n                items = videos,\n                key = { index, seasonCardData -> \"$index-season-${seasonCardData.seasonId}\" }\n            ) { _, seasonCardData ->\n                SeasonCard(\n                    modifier = Modifier.width(150.dp),\n                    data = seasonCardData,\n                    onClick = {\n                        SeasonInfoActivity.actionStart(\n                            context = context,\n                            seasonId = seasonCardData.seasonId,\n                            proxyArea = ProxyArea.checkProxyArea(seasonCardData.title)\n                        )\n                    }\n                )\n            }\n            item {\n                Button(\n                    modifier = Modifier.height(rowHeight),\n                    shape = ButtonDefaults.shape(shape = MaterialTheme.shapes.medium),\n                    onClick = showMore\n                ) {\n                    Column(\n                        modifier = Modifier.fillMaxHeight(),\n                        verticalArrangement = Arrangement.Center,\n                    ) {\n                        Text(text = \"显示更多\")\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun FavoriteVideosRow(\n    modifier: Modifier = Modifier,\n    videos: List<VideoCardData>,\n    showMore: () -> Unit\n) {\n    val context = LocalContext.current\n    VideosRow(\n        modifier = modifier\n            .padding(vertical = 8.dp),\n        header = stringResource(R.string.user_homepage_favorite),\n        hideShowMore = false,\n        showMore = showMore,\n        videos = videos,\n        onOpenSeasonInfo = { videoData ->\n            SeasonInfoActivity.actionStart(\n                context = context,\n                epId = videoData.epId!!,\n                proxyArea = ProxyArea.checkProxyArea(videoData.title)\n            )\n        },\n        onOpenVideoInfo = { videoData ->\n            VideoInfoActivity.actionStart(context, videoData.avid)\n        }\n    )\n}\n\n@Preview\n@Composable\nprivate fun UserInfoPreview() {\n    BVTheme {\n        UserInfo(\n            face = \"\",\n            username = \"Username\",\n            uid = 12345,\n            level = 6,\n            currentExp = 1234,\n            nextLevelExp = 2345,\n            showLabel = false,\n            labelUrl = \"\",\n            onClick = {}\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun UserInfoFocusedPreview() {\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus()\n    }\n\n    BVTheme {\n        UserInfo(\n            modifier = Modifier.focusRequester(focusRequester),\n            face = \"\",\n            username = \"Username\",\n            uid = 12345,\n            level = 6,\n            currentExp = 1234,\n            nextLevelExp = 2345,\n            showLabel = false,\n            labelUrl = \"\",\n            onClick = {}\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun IncognitoModeCardOnPreview() {\n    BVTheme {\n        IncognitoModeCardContent(\n            enabled = true,\n            onClick = {}\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun IncognitoModeCardOffPreview() {\n    BVTheme {\n        IncognitoModeCardContent(\n            enabled = false,\n            onClick = {}\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun FollowedUserCardPreview() {\n    BVTheme {\n        FollowedUserCard(\n            size = 466,\n            onClick = {}\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun UserSwitchCardPreview() {\n    BVTheme {\n        UserSwitchCard(\n            onClick = {}\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun UserRowPreview() {\n    BVTheme {\n        UserRow(\n            username = \"Username\",\n            face = \"\",\n            uid = 1234567890,\n            level = 4,\n            currentExp = 123,\n            nextLevelExp = 431,\n            showLabel = false,\n            labelUrl = \"\",\n            followingUpCount = 466,\n            onOpenFollowingUser = { },\n            onOpenUserSwitch = {}\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/UserSwitchScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.content.res.Configuration\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ExitToApp\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Lock\nimport androidx.compose.material.icons.filled.Settings\nimport androidx.compose.material3.BadgedBox\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport androidx.tv.material3.Border\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.ButtonDefaults\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.Glow\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.OutlinedButton\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.SurfaceDefaults\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.component.QrImage\nimport dev.aaa1115910.bv.entity.BvScheme\nimport dev.aaa1115910.bv.entity.db.UserDB\nimport dev.aaa1115910.bv.repository.UserRepository\nimport dev.aaa1115910.bv.tv.activities.user.LoginActivity\nimport dev.aaa1115910.bv.tv.activities.user.UserLockSettingsActivity\nimport dev.aaa1115910.bv.tv.component.TvAlertDialog\nimport dev.aaa1115910.bv.tv.screens.user.lock.UnlockSwitchUserContent\nimport dev.aaa1115910.bv.ui.theme.BVTheme\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.viewmodel.UserSwitchViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\nimport org.koin.compose.getKoin\n\n@Composable\nfun UserSwitchScreen(\n    modifier: Modifier = Modifier,\n    userSwitchViewModel: UserSwitchViewModel = koinViewModel(),\n    userRepository: UserRepository = getKoin().get()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val lifecycleOwner = LocalLifecycleOwner.current\n\n    val userList = userSwitchViewModel.userDbList\n\n    var showUnlock by remember { mutableStateOf(false) }\n    var unlockUser: UserDB? by remember { mutableStateOf(null) }\n\n    DisposableEffect(lifecycleOwner) {\n        val observer = LifecycleEventObserver { _, event ->\n            if (event == Lifecycle.Event.ON_RESUME) {\n                scope.launch {\n                    //userSwitchViewModel.updateUserDbList()\n                    userSwitchViewModel.updateData()\n                }\n            }\n        }\n\n        lifecycleOwner.lifecycle.addObserver(observer)\n\n        onDispose {\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n\n    val unlockFocusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(showUnlock) {\n        if (showUnlock) unlockFocusRequester.requestFocus()\n    }\n\n    Surface(\n        modifier = modifier,\n        shape = RoundedCornerShape(0.dp)\n    ) {\n        Box {\n            UserSwitchContent(\n                userList = userList,\n                currentUid = userRepository.uid,\n                loadingUserList = userSwitchViewModel.loading,\n                onAddUser = {\n                    context.startActivity(Intent(context, LoginActivity::class.java))\n                },\n                onDeleteUser = { user ->\n                    scope.launch(Dispatchers.IO) {\n                        userSwitchViewModel.deleteUser(user)\n                        if (userList.isEmpty()) (context as Activity).finish()\n                    }\n                },\n                onSwitchUser = { user ->\n                    if (user.uid != userRepository.uid && user.lock.isNotBlank()) {\n                        unlockUser = user\n                        showUnlock = true\n                    } else {\n                        scope.launch(Dispatchers.IO) {\n                            userSwitchViewModel.switchUser(user)\n                            (context as Activity).finish()\n                        }\n                    }\n                },\n                onShowUserLockSettings = { uid ->\n                    UserLockSettingsActivity.actionStart(context, uid)\n                }\n            )\n\n            if (showUnlock) {\n                UnlockSwitchUserContent(\n                    modifier = Modifier.focusRequester(unlockFocusRequester),\n                    userList = userList,\n                    unlockUser = unlockUser!!,\n                    onUnlockSuccess = { user ->\n                        scope.launch(Dispatchers.IO) {\n                            userSwitchViewModel.switchUser(user)\n                            (context as Activity).finish()\n                        }\n                    },\n                    onCancel = {\n                        showUnlock = false\n                    }\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun UserSwitchContent(\n    modifier: Modifier = Modifier,\n    userList: List<UserDB> = emptyList(),\n    currentUid: Long,\n    loadingUserList: Boolean,\n    onSwitchUser: (UserDB) -> Unit,\n    onDeleteUser: (UserDB) -> Unit,\n    onAddUser: () -> Unit,\n    onShowUserLockSettings: (Long) -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n    var choosedUser by remember {\n        mutableStateOf(\n            UserDB(\n                uid = -1,\n                username = \"None\",\n                avatar = \"https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg\",\n                auth = \"\"\n            )\n        )\n    }\n\n    var isInManagerMode by remember { mutableStateOf(false) }\n    var showUserMenuDialog by remember { mutableStateOf(false) }\n    var showAuthDataDialog by remember { mutableStateOf(false) }\n    var showDeleteConfirmDialog by remember { mutableStateOf(false) }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus()\n    }\n\n    LaunchedEffect(loadingUserList) {\n        if (!loadingUserList) focusRequester.requestFocus()\n    }\n\n    Surface(\n        modifier = modifier,\n        shape = RoundedCornerShape(0.dp)\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Column(\n                modifier = Modifier\n                    .align(Alignment.TopCenter)\n                    .padding(top = 64.dp),\n                horizontalAlignment = Alignment.CenterHorizontally\n            ) {\n                Text(\n                    text = stringResource(R.string.user_switch_title),\n                    style = MaterialTheme.typography.displaySmall\n                )\n            }\n\n            LazyRow(\n                modifier = Modifier.focusRequester(focusRequester),\n                horizontalArrangement = Arrangement.spacedBy(24.dp),\n                contentPadding = PaddingValues(horizontal = 12.dp)\n            ) {\n                itemsIndexed(\n                    items = userList,\n                    key = { index, user -> \"$index-user-${user.uid}\" }\n                ) { _, user ->\n                    UserItem(\n                        avatar = user.avatar,\n                        username = user.username,\n                        lockEnabled = user.lock.isNotBlank(),\n                        onClick = {\n                            if (isInManagerMode) {\n                                choosedUser = user\n                                showUserMenuDialog = true\n                            } else {\n                                onSwitchUser(user)\n                            }\n                        }\n                    )\n                }\n                if (!isInManagerMode) {\n                    item {\n                        AddUserItem(\n                            onClick = onAddUser\n                        )\n                    }\n                }\n            }\n\n            Button(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .padding(bottom = 64.dp),\n                onClick = { isInManagerMode = !isInManagerMode }\n            ) {\n                if (isInManagerMode) {\n                    Row(\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.ExitToApp,\n                            contentDescription = null\n                        )\n                        Text(stringResource(R.string.user_switch_button_exit_manage_account))\n                    }\n                } else {\n                    Row(\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Icon(imageVector = Icons.Default.Settings, contentDescription = null)\n                        Text(stringResource(R.string.user_switch_button_manage_account))\n                    }\n                }\n            }\n        }\n    }\n\n    UserMenuDialog(\n        show = showUserMenuDialog,\n        onHideDialog = { showUserMenuDialog = false },\n        username = choosedUser.username,\n        uid = choosedUser.uid,\n        showTokenButton = choosedUser.uid == currentUid || choosedUser.lock.isBlank(),\n        onShowUserAuthData = { showAuthDataDialog = true },\n        onDeleteUser = { showDeleteConfirmDialog = true },\n        onShowUserLockSettings = { uid ->\n            isInManagerMode = false\n            onShowUserLockSettings(uid)\n        }\n    )\n\n    UserAuthDataDialog(\n        show = showAuthDataDialog,\n        onHideDialog = { showAuthDataDialog = false },\n        userDB = choosedUser\n    )\n\n    DeleteConfirmDialog(\n        show = showDeleteConfirmDialog,\n        onHideDialog = { showDeleteConfirmDialog = false },\n        userDB = choosedUser,\n        onConfirm = {\n            onDeleteUser(choosedUser)\n            showDeleteConfirmDialog = false\n        }\n    )\n}\n\n@Composable\nfun UserMenuDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    username: String,\n    uid: Long,\n    showTokenButton: Boolean,\n    onShowUserAuthData: () -> Unit,\n    onDeleteUser: () -> Unit,\n    onShowUserLockSettings: (Long) -> Unit\n) {\n    val menuFocusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(show) {\n        if (show) {\n            menuFocusRequester.requestFocus()\n        }\n    }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = onHideDialog,\n            title = { Text(text = username) },\n            text = {\n                LazyColumn(\n                    modifier = Modifier.width(240.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp),\n                    contentPadding = PaddingValues(horizontal = 12.dp)\n                ) {\n                    if (showTokenButton) {\n                        item {\n                            UserMenuButton(\n                                modifier = Modifier.focusRequester(menuFocusRequester),\n                                text = stringResource(R.string.user_switch_menu_show_token),\n                                onClick = {\n                                    onHideDialog()\n                                    onShowUserAuthData()\n                                }\n                            )\n                        }\n                    }\n\n                    item {\n                        UserMenuButton(\n                            modifier = Modifier\n                                .ifElse(\n                                    !showTokenButton,\n                                    Modifier.focusRequester(menuFocusRequester)\n                                ),\n                            text = stringResource(R.string.user_switch_menu_user_lock),\n                            onClick = {\n                                onHideDialog()\n                                onShowUserLockSettings(uid)\n                            }\n                        )\n                    }\n\n                    item {\n                        UserMenuButton(\n                            text = stringResource(R.string.user_switch_menu_delete_account),\n                            onClick = {\n                                onHideDialog()\n                                onDeleteUser()\n                            },\n                            color = MaterialTheme.colorScheme.errorContainer\n                        )\n                    }\n                }\n            },\n            dismissButton = {},\n            confirmButton = {}\n        )\n    }\n}\n\n@Composable\nfun UserAuthDataDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    userDB: UserDB\n) {\n    var qrContent by remember { mutableStateOf(\"\") }\n\n    LaunchedEffect(show) {\n        if (show) {\n            qrContent = BvScheme.QrToken(\n                auth = userDB.auth,\n                uid = userDB.uid,\n                username = userDB.username,\n                avatar = userDB.avatar\n            ).buildUri()\n        }\n    }\n\n    BackHandler(show) { onHideDialog() }\n\n    if (show) {\n        Scaffold(\n            modifier\n                .fillMaxSize(),\n            topBar = {\n                Box(\n                    modifier = Modifier.padding(start = 48.dp, top = 24.dp, bottom = 8.dp)\n                ) {\n                    Text(\n                        text = userDB.username,\n                        fontSize = 48.sp\n                    )\n                }\n            }\n        ) { innerPadding ->\n            Box(\n                modifier = Modifier.padding(innerPadding)\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .weight(4f)\n                            .fillMaxHeight(),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        QrImage(\n                            modifier = Modifier\n                                .size(240.dp),\n                            content = qrContent\n                        )\n                    }\n\n                    Box(\n                        modifier = Modifier\n                            .weight(6f)\n                            .padding(end = 60.dp)\n                    ) {\n                        Column(\n                            verticalArrangement = Arrangement.spacedBy(24.dp)\n                        ) {\n                            Text(\n                                text = \"扫码二维码以登录移动端\",\n                                style = MaterialTheme.typography.displaySmall\n                            )\n\n                            Text(text = userDB.auth)\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun DeleteConfirmDialog(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onHideDialog: () -> Unit,\n    userDB: UserDB,\n    onConfirm: () -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(show) {\n        if (show) focusRequester.requestFocus(scope)\n    }\n\n    if (show) {\n        TvAlertDialog(\n            modifier = modifier,\n            onDismissRequest = { onHideDialog() },\n            title = { Text(text = stringResource(R.string.delete_account_confirm_dialog_title)) },\n            text = {\n                Text(\n                    text = stringResource(\n                        R.string.delete_account_confirm_dialog_text,\n                        userDB.username,\n                        userDB.uid\n                    )\n                )\n            },\n            confirmButton = {\n                Button(onClick = { onConfirm() }) {\n                    Text(text = stringResource(R.string.delete_account_confirm_dialog_confirm))\n                }\n            },\n            dismissButton = {\n                OutlinedButton(\n                    modifier = Modifier.focusRequester(focusRequester),\n                    onClick = { onHideDialog() }\n                ) {\n                    Text(text = stringResource(R.string.delete_account_confirm_dialog_dismiss))\n                }\n            }\n        )\n    }\n}\n\n@Composable\nfun UserItem(\n    modifier: Modifier = Modifier,\n    avatar: String,\n    username: String,\n    lockEnabled: Boolean = false,\n    onClick: (() -> Unit)? = null\n) {\n    Column(\n        modifier = modifier.width(120.dp),\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        if (onClick != null) {\n            BadgedBox(\n                modifier = Modifier.padding(18.dp),\n                badge = {\n                    if (lockEnabled) {\n                        Icon(imageVector = Icons.Default.Lock, contentDescription = null)\n                    }\n                }\n            ) {\n                Surface(\n                    modifier = Modifier\n                        .size(80.dp),\n                    colors = ClickableSurfaceDefaults.colors(\n                        containerColor = MaterialTheme.colorScheme.surfaceVariant,\n                        focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant\n                    ),\n                    shape = ClickableSurfaceDefaults.shape(\n                        shape = CircleShape\n                    ),\n                    glow = ClickableSurfaceDefaults.glow(\n                        focusedGlow = Glow(\n                            elevationColor = MaterialTheme.colorScheme.border,\n                            elevation = 16.dp\n                        )\n                    ),\n                    onClick = onClick,\n                    border = ClickableSurfaceDefaults.border(\n                        focusedBorder = Border(\n                            BorderStroke(\n                                width = 3.dp,\n                                color = MaterialTheme.colorScheme.border.copy(alpha = 0.7f)\n                            )\n                        )\n                    )\n                ) {\n                    AsyncImage(\n                        modifier = Modifier\n                            .size(80.dp)\n                            .clip(CircleShape),\n                        model = avatar,\n                        contentDescription = null,\n                        contentScale = ContentScale.FillBounds\n                    )\n                }\n            }\n        } else {\n            Surface(\n                modifier = Modifier\n                    .padding(18.dp)\n                    .size(80.dp),\n                colors = SurfaceDefaults.colors(\n                    containerColor = Color.DarkGray\n                ),\n                shape = CircleShape\n            ) {\n                AsyncImage(\n                    modifier = Modifier\n                        .size(80.dp)\n                        .clip(CircleShape),\n                    model = avatar,\n                    contentDescription = null,\n                    contentScale = ContentScale.FillBounds\n                )\n            }\n        }\n        Box(\n            modifier = Modifier.height(26.dp),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .basicMarquee(),\n                text = username,\n                style = MaterialTheme.typography.titleMedium,\n                maxLines = 1,\n                textAlign = TextAlign.Center\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun AddUserItem(\n    modifier: Modifier = Modifier,\n    onClick: () -> Unit\n) {\n    Column(\n        modifier = modifier.width(120.dp),\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        Surface(\n            modifier = Modifier\n                .padding(18.dp)\n                .size(80.dp),\n            colors = ClickableSurfaceDefaults.colors(\n                containerColor = MaterialTheme.colorScheme.surfaceVariant,\n                focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant\n            ),\n            shape = ClickableSurfaceDefaults.shape(\n                shape = CircleShape\n            ),\n            glow = ClickableSurfaceDefaults.glow(\n                focusedGlow = Glow(\n                    elevationColor = MaterialTheme.colorScheme.inverseSurface,\n                    elevation = 16.dp\n                )\n            ),\n            onClick = onClick,\n            border = ClickableSurfaceDefaults.border(\n                focusedBorder = Border(\n                    BorderStroke(\n                        width = 3.dp,\n                        color = MaterialTheme.colorScheme.border.copy(alpha = 0.7f)\n                    )\n                )\n            )\n        ) {\n            Box(\n                modifier = Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center\n            ) {\n                Icon(\n                    modifier = Modifier.size(40.dp),\n                    imageVector = Icons.Default.Add,\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n            }\n        }\n        Box(\n            modifier = Modifier.height(26.dp),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .basicMarquee(),\n                text = stringResource(R.string.user_switch_add_user),\n                style = MaterialTheme.typography.titleMedium,\n                maxLines = 1,\n                textAlign = TextAlign.Center\n            )\n        }\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun UserItemPreview() {\n    BVTheme {\n        UserItem(\n            avatar = \"\",\n            username = \"This is a user name\",\n            onClick = {},\n            lockEnabled = true\n        )\n    }\n}\n\n@Preview\n@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun AddUserItemPreview() {\n    BVTheme {\n        AddUserItem(\n            onClick = {}\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun UserSwitchContentPreview() {\n    BVTheme {\n        UserSwitchContent(\n            userList = listOf(\n                UserDB(\n                    uid = 0,\n                    username = \"大楚兴 陈胜王 大楚兴 陈胜王\",\n                    avatar = \"0https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg\",\n                    auth = \"{xxx1}\"\n                ),\n                UserDB(\n                    uid = 1,\n                    username = \"This is a long username\",\n                    avatar = \"0https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg\",\n                    auth = \"{xxx2}\",\n                    lock = \"rdrd\"\n                ),\n                UserDB(\n                    uid = 2,\n                    username = \"\\uD835\\uDD4F\",\n                    avatar = \"0https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg\",\n                    auth = \"{xxx3}\"\n                )\n            ),\n            currentUid = 0L,\n            loadingUserList = false,\n            onSwitchUser = {},\n            onDeleteUser = {},\n            onAddUser = {},\n            onShowUserLockSettings = {}\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun UserMenuDialogPreview() {\n    BVTheme {\n        UserMenuDialog(\n            show = true,\n            onHideDialog = {},\n            username = \"This is a user name\",\n            uid = 0,\n            showTokenButton = true,\n            onShowUserAuthData = {},\n            onDeleteUser = {},\n            onShowUserLockSettings = {}\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Preview(device = \"id:tv_1080p\", uiMode = Configuration.UI_MODE_NIGHT_YES)\n@Composable\nfun UserAuthDataDialogPreview() {\n    BVTheme {\n        UserAuthDataDialog(\n            show = true,\n            onHideDialog = {},\n            userDB = UserDB(\n                uid = 0,\n                username = \"Android Studio Official\",\n                avatar = \"0https://i0.hdslb.com/bfs/article/b6b843d84b84a3ba5526b09ebf538cd4b4c8c3f3.jpg\",\n                auth = \"this is a long auth data string that is used to test the dialog layout and should be long enough to wrap into multiple lines.\"\n            ),\n        )\n    }\n}\n\n@Composable\nprivate fun UserMenuButton(\n    modifier: Modifier = Modifier,\n    text: String,\n    onClick: () -> Unit,\n    color: Color? = null\n) {\n    Button(\n        modifier = modifier\n            .fillMaxWidth()\n            .height(48.dp),\n        shape = ButtonDefaults.shape(shape = MaterialTheme.shapes.medium),\n        colors = if (color != null) ButtonDefaults.colors(containerColor = color) else ButtonDefaults.colors(),\n        onClick = onClick\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Text(\n                text = text,\n                style = MaterialTheme.typography.bodyLarge,\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/lock/UnlockSwitchUserContent.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user.lock\n\nimport android.view.KeyEvent\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.entity.db.UserDB\nimport dev.aaa1115910.bv.tv.screens.user.UserItem\nimport dev.aaa1115910.bv.util.toast\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\n@Composable\nfun UnlockSwitchUserContent(\n    modifier: Modifier = Modifier,\n    userList: List<UserDB>,\n    unlockUser: UserDB?,\n    onUnlockSuccess: (UserDB) -> Unit,\n    onCancel: () -> Unit\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n\n    val defaultFocusRequester = remember { FocusRequester() }\n    var inputPassword by remember { mutableStateOf(\"\") }\n    val inputShow by remember {\n        derivedStateOf {\n            inputPassword\n                .replace(\"u\", \"*\")\n                .replace(\"d\", \"*\")\n                .replace(\"l\", \"*\")\n                .replace(\"r\", \"*\")\n        }\n    }\n    val unselectedUserAlpha by remember { mutableFloatStateOf(0.4f) }\n\n    LaunchedEffect(Unit) {\n        scope.launch {\n            delay(200)\n            println(\"request default focus\")\n            defaultFocusRequester.requestFocus()\n        }\n    }\n\n    BackHandler(true) {\n    }\n\n    Surface(\n        modifier = modifier\n            .clickable {}\n            .focusRequester(defaultFocusRequester)\n            .onPreviewKeyEvent {\n                if (it.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) return@onPreviewKeyEvent true\n                when (it.key) {\n                    Key.DirectionUp -> inputPassword += \"u\"\n                    Key.DirectionDown -> inputPassword += \"d\"\n                    Key.DirectionLeft -> inputPassword += \"l\"\n                    Key.DirectionRight -> inputPassword += \"r\"\n                    Key.DirectionCenter -> {\n                        if (unlockUser?.lock == inputPassword) {\n                            onUnlockSuccess(unlockUser)\n                        } else {\n                            R.string.user_lock_toast_password_error.toast(context)\n                            inputPassword = \"\"\n                        }\n                    }\n\n                    Key.Back -> {\n                        if (inputPassword.isNotBlank()) {\n                            inputPassword = inputPassword.drop(1)\n                        } else {\n                            onCancel()\n                        }\n                    }\n                }\n                return@onPreviewKeyEvent true\n            },\n        shape = RoundedCornerShape(0.dp)\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Column(\n                modifier = Modifier\n                    .align(Alignment.TopCenter)\n                    .padding(top = 64.dp),\n                horizontalAlignment = Alignment.CenterHorizontally\n            ) {\n                Text(\n                    text = stringResource(R.string.user_lock_title_input_password),\n                    style = MaterialTheme.typography.displaySmall\n                )\n            }\n\n            LazyRow(\n                horizontalArrangement = Arrangement.spacedBy(24.dp),\n                contentPadding = PaddingValues(horizontal = 12.dp)\n            ) {\n                itemsIndexed(\n                    items = userList,\n                    key = { index, user -> \"$index-user-${user.uid}\" }\n                ) { _, user ->\n                    UserItem(\n                        modifier = Modifier\n                            .ifElse({ user != unlockUser }, Modifier.alpha(unselectedUserAlpha)),\n                        avatar = user.avatar,\n                        username = user.username,\n                        lockEnabled = user.lock.isNotBlank(),\n                    )\n                }\n            }\n\n            Text(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .padding(bottom = 96.dp),\n                text = inputShow,\n                style = MaterialTheme.typography.displayLarge\n            )\n\n            Text(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .padding(bottom = 48.dp),\n                text = stringResource(R.string.user_lock_input_tip),\n                color = MaterialTheme.colorScheme.onSurface.copy(0.6f)\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/lock/UnlockUserScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user.lock\n\nimport android.view.KeyEvent\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.entity.db.UserDB\nimport dev.aaa1115910.bv.tv.screens.user.UserItem\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.util.toast\nimport dev.aaa1115910.bv.viewmodel.UserSwitchViewModel\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.koin.androidx.compose.koinViewModel\n\n@Composable\nfun UnlockUserScreen(\n    modifier: Modifier = Modifier,\n    userSwitchViewModel: UserSwitchViewModel = koinViewModel(),\n    onUnlockSuccess: (UserDB) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val userList = userSwitchViewModel.userDbList\n    var selectedUser: UserDB? by remember { mutableStateOf(null) }\n\n    LaunchedEffect(Unit) {\n        userSwitchViewModel.updateData()\n    }\n\n    UnlockUserContent(\n        modifier = modifier,\n        userList = userList,\n        selectedUser = selectedUser,\n        onSelectedUserChange = { user ->\n            selectedUser = user\n        },\n        onUnlockSuccess = { user ->\n            scope.launch {\n                userSwitchViewModel.switchUser(user)\n                onUnlockSuccess(user)\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun UnlockUserContent(\n    modifier: Modifier = Modifier,\n    userList: List<UserDB>,\n    selectedUser: UserDB?,\n    onSelectedUserChange: (UserDB) -> Unit,\n    onUnlockSuccess: (UserDB) -> Unit\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger(\"UnlockUserContent\")\n\n    val inputFocusRequester = remember { FocusRequester() }\n    val defaultFocusRequester = remember { FocusRequester() }\n    var inputPassword by remember { mutableStateOf(\"\") }\n    val inputShow by remember {\n        derivedStateOf {\n            inputPassword\n                .replace(\"u\", \"*\")\n                .replace(\"d\", \"*\")\n                .replace(\"l\", \"*\")\n                .replace(\"r\", \"*\")\n        }\n    }\n    var unlockState by remember { mutableStateOf(UnlockState.ChooseUser) }\n    val unChosenUserAlpha by animateFloatAsState(\n        targetValue = when (unlockState) {\n            UnlockState.ChooseUser -> 1f\n            UnlockState.InputPassword -> 0.4f\n        },\n        label = \"unchosen user alpha\"\n    )\n\n    LaunchedEffect(userList) {\n        scope.launch {\n            delay(200)\n            defaultFocusRequester.requestFocus(scope)\n        }\n    }\n\n    LaunchedEffect(unlockState) {\n        scope.launch {\n            delay(100)\n            inputFocusRequester.requestFocus()\n        }\n    }\n\n    BackHandler(true) {\n    }\n\n    Surface(\n        modifier = modifier\n            .ifElse({ unlockState == UnlockState.InputPassword }, Modifier.clickable {})\n            .focusRequester(inputFocusRequester)\n            .onPreviewKeyEvent {\n                when (unlockState) {\n                    UnlockState.ChooseUser -> return@onPreviewKeyEvent false\n                    UnlockState.InputPassword -> {\n                        if (it.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) return@onPreviewKeyEvent true\n                        when (it.key) {\n                            Key.DirectionUp -> inputPassword += \"u\"\n                            Key.DirectionDown -> inputPassword += \"d\"\n                            Key.DirectionLeft -> inputPassword += \"l\"\n                            Key.DirectionRight -> inputPassword += \"r\"\n                            Key.DirectionCenter -> {\n                                if (selectedUser?.lock == inputPassword) {\n                                    onUnlockSuccess(selectedUser)\n                                } else {\n                                    \"密码错误\".toast(context)\n                                    inputPassword = \"\"\n                                }\n                            }\n\n                            Key.Back -> {\n                                if (inputPassword.isNotBlank()) {\n                                    inputPassword = inputPassword.drop(1)\n                                } else {\n                                    unlockState = UnlockState.ChooseUser\n                                    defaultFocusRequester.requestFocus()\n                                }\n                            }\n                        }\n                        return@onPreviewKeyEvent true\n                    }\n                }\n            },\n        shape = RoundedCornerShape(0.dp)\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Column(\n                modifier = Modifier\n                    .align(Alignment.TopCenter)\n                    .padding(top = 64.dp),\n                horizontalAlignment = Alignment.CenterHorizontally\n            ) {\n                Text(\n                    text = when (unlockState) {\n                        UnlockState.ChooseUser -> stringResource(R.string.user_lock_title_choose_user)\n                        UnlockState.InputPassword -> stringResource(R.string.user_lock_title_input_password)\n                    },\n                    style = MaterialTheme.typography.displaySmall\n                )\n            }\n\n            LazyRow(\n                modifier = Modifier.focusRequester(defaultFocusRequester),\n                horizontalArrangement = Arrangement.spacedBy(24.dp),\n                contentPadding = PaddingValues(horizontal = 12.dp)\n            ) {\n                itemsIndexed(\n                    items = userList,\n                    key = { index, user -> \"$index-user-${user.uid}\" }\n                ) { _, user ->\n                    UserItem(\n                        modifier = Modifier\n                            .ifElse({ user != selectedUser }, Modifier.alpha(unChosenUserAlpha)),\n                        avatar = user.avatar,\n                        username = user.username,\n                        lockEnabled = user.lock.isNotBlank(),\n                        onClick = {\n                            logger.info { \"Choose user ${user.uid}\" }\n                            if (user.lock.isNotBlank()) {\n                                onSelectedUserChange(user)\n                                unlockState = UnlockState.InputPassword\n                            } else {\n                                onSelectedUserChange(user)\n                                onUnlockSuccess(user)\n                            }\n                        }.takeIf { unlockState == UnlockState.ChooseUser }\n                    )\n                }\n            }\n\n            Text(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .padding(bottom = 96.dp),\n                text = inputShow,\n                style = MaterialTheme.typography.displayLarge\n            )\n\n            if (unlockState == UnlockState.InputPassword) {\n                Text(\n                    modifier = Modifier\n                        .align(Alignment.BottomCenter)\n                        .padding(bottom = 48.dp),\n                    text = stringResource(R.string.user_lock_input_tip),\n                    color = MaterialTheme.colorScheme.onSurface.copy(0.6f)\n                )\n            }\n        }\n    }\n}\n\nprivate enum class UnlockState {\n    ChooseUser,\n    InputPassword\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/screens/user/lock/UserLockSettingsScreen.kt",
    "content": "package dev.aaa1115910.bv.tv.screens.user.lock\n\nimport android.app.Activity\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.R\nimport dev.aaa1115910.bv.entity.db.UserDB\nimport dev.aaa1115910.bv.repository.UserRepository\nimport dev.aaa1115910.bv.tv.screens.user.UserItem\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.launch\nimport org.koin.compose.getKoin\n\n@Composable\nfun UserLockSettingsScreen(\n    modifier: Modifier = Modifier,\n    userRepository: UserRepository = getKoin().get()\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger(\"UserLockSettingsScreen\")\n\n    var user by remember {\n        mutableStateOf(\n            UserDB(\n                uid = -1,\n                username = \"None\",\n                avatar = \"\",\n                auth = \"\"\n            )\n        )\n    }\n\n    LaunchedEffect(Unit) {\n        val intent = (context as Activity).intent\n        if (intent.hasExtra(\"uid\")) {\n            val uid = intent.getLongExtra(\"uid\", 0)\n            userRepository.findUserByUid(uid)\n                ?.let { user = it }\n                ?: let { context.finish() }\n            logger.debug { \"user $uid lock: ${user.lock}\" }\n        } else {\n            context.finish()\n        }\n    }\n\n    UserLockSettingsContent(\n        modifier = modifier,\n        user = user,\n        onUpdateUser = {\n            scope.launch {\n                userRepository.updateUser(it)\n                (context as Activity).finish()\n            }\n        },\n        onExit = {\n            (context as Activity).finish()\n        }\n    )\n}\n\n@Composable\nprivate fun UserLockSettingsContent(\n    modifier: Modifier = Modifier,\n    user: UserDB,\n    onUpdateUser: (UserDB) -> Unit,\n    onExit: () -> Unit\n) {\n    val context = LocalContext.current\n\n    val focusRequester = remember { FocusRequester() }\n    var inputState by remember { mutableStateOf(InputState.InputOldPassword) }\n    var inputPassword by remember { mutableStateOf(\"\") }\n    var lastInput by remember { mutableStateOf(\"\") }\n    val inputShow by remember {\n        derivedStateOf {\n            inputPassword\n                .replace(\"u\", \"↑\")\n                .replace(\"d\", \"↓\")\n                .replace(\"l\", \"←\")\n                .replace(\"r\", \"→\")\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus()\n\n    }\n    LaunchedEffect(user) {\n        inputState = if (user.lock.isNotBlank()) InputState.InputOldPassword\n        else InputState.InputNewPassword\n    }\n\n    BackHandler(inputPassword.isNotEmpty()) {\n    }\n\n    Surface(\n        modifier = modifier\n            .clickable {}\n            .focusRequester(focusRequester)\n            .onPreviewKeyEvent { keyEvent ->\n                if (keyEvent.nativeKeyEvent.action == android.view.KeyEvent.ACTION_DOWN) {\n                    return@onPreviewKeyEvent true\n                }\n\n                when (keyEvent.key) {\n                    Key.DirectionUp -> inputPassword += \"u\"\n                    Key.DirectionDown -> inputPassword += \"d\"\n                    Key.DirectionLeft -> inputPassword += \"l\"\n                    Key.DirectionRight -> inputPassword += \"r\"\n\n                    Key.DirectionCenter -> {\n                        when (inputState) {\n                            InputState.InputOldPassword -> {\n                                if (inputPassword == user.lock) {\n                                    inputState = InputState.InputNewPassword\n                                    inputPassword = \"\"\n                                } else {\n                                    R.string.user_lock_toast_password_error.toast(context)\n                                    inputPassword = \"\"\n                                }\n                            }\n\n                            InputState.InputNewPassword -> {\n                                if (inputPassword.isBlank()) {\n                                    R.string.user_lock_toast_password_removed.toast(context)\n                                    user.lock = \"\"\n                                    onUpdateUser(user)\n                                } else {\n                                    lastInput = inputPassword\n                                    inputPassword = \"\"\n                                    inputState = InputState.ConfirmNewPassword\n                                }\n                            }\n\n                            InputState.ConfirmNewPassword -> {\n                                if (inputPassword == lastInput) {\n                                    user.lock = inputPassword\n                                    onUpdateUser(user)\n                                } else {\n                                    R.string.user_lock_toast_password_different.toast(context)\n                                    inputPassword = \"\"\n                                    inputState = InputState.InputNewPassword\n                                }\n                            }\n                        }\n                    }\n\n                    Key.Back -> {\n                        if (inputPassword.isNotEmpty()) {\n                            inputPassword = inputPassword.dropLast(1)\n                        } else {\n                            onExit()\n                        }\n                    }\n                }\n                true\n            },\n        shape = RoundedCornerShape(0.dp)\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.Center\n        ) {\n            Column(\n                modifier = Modifier\n                    .align(Alignment.TopCenter)\n                    .padding(top = 64.dp),\n                horizontalAlignment = Alignment.CenterHorizontally\n            ) {\n                Text(\n                    text = when (inputState) {\n                        InputState.InputOldPassword -> stringResource(R.string.user_lock_title_input_old_password)\n                        InputState.InputNewPassword -> stringResource(R.string.user_lock_title_input_new_password)\n                        InputState.ConfirmNewPassword -> stringResource(R.string.user_lock_title_input_new_password_again)\n                    },\n                    style = MaterialTheme.typography.displaySmall\n                )\n            }\n\n            LazyRow(\n                modifier = Modifier.focusRequester(focusRequester),\n                horizontalArrangement = Arrangement.spacedBy(24.dp),\n                contentPadding = PaddingValues(horizontal = 12.dp)\n            ) {\n                item {\n                    UserItem(\n                        avatar = user.avatar,\n                        username = user.username\n                    )\n                }\n            }\n\n            Text(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .padding(bottom = 96.dp),\n                text = inputShow,\n                style = MaterialTheme.typography.displayLarge\n            )\n\n            Text(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .padding(bottom = 48.dp),\n                text = stringResource(R.string.user_lock_input_tip),\n                color = MaterialTheme.colorScheme.onSurface.copy(0.6f)\n            )\n        }\n    }\n}\n\nprivate enum class InputState {\n    InputOldPassword,\n    InputNewPassword,\n    ConfirmNewPassword\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/util/NavItemsExtensions.kt",
    "content": "package dev.aaa1115910.bv.tv.util\n\nimport android.content.Context\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport dev.aaa1115910.biliapi.entity.live.LiveAreaGroup\nimport dev.aaa1115910.bv.tv.component.HomeTopNavItem\nimport dev.aaa1115910.bv.tv.component.PgcTopNavItem\nimport dev.aaa1115910.bv.tv.component.TopNavItem\nimport dev.aaa1115910.bv.tv.component.UgcTopNavItem\nimport dev.aaa1115910.bv.tv.screens.main.DrawerItem\nimport dev.aaa1115910.bv.util.Prefs\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\n\n/**\n * 导航项配置数据类\n */\ndata class NavItemConfig(\n    val ordinal: Int,\n    val hidden: Boolean\n)\n\n// ======================== 直播导航项配置 ========================\n\n/**\n * 直播导航项配置数据类\n * @param id 标识符：\"R\"=推荐, \"F\"=关注, 数字字符串=主分区ID\n * @param hidden 是否隐藏\n */\ndata class LiveNavItemConfig(\n    val id: String,\n    val hidden: Boolean\n)\n\n/**\n * 缓存的直播分区信息\n */\ndata class CachedLiveAreaInfo(\n    val id: Int,\n    val name: String\n)\n\n/**\n * 序列化直播分区列表为缓存字符串\n * 格式: \"id1:name1,id2:name2,...\"\n */\nfun serializeLiveAreaGroups(areaGroups: List<LiveAreaGroup>): String {\n    return areaGroups.joinToString(\",\") { \"${it.id}:${it.name}\" }\n}\n\n/**\n * 解析缓存的直播分区字符串\n * @return 分区ID和名称的列表\n */\nfun parseCachedLiveAreaGroups(cacheString: String): List<CachedLiveAreaInfo> {\n    if (cacheString.isBlank()) return emptyList()\n    return cacheString.split(\",\").mapNotNull { part ->\n        val colonIndex = part.indexOf(':')\n        if (colonIndex < 0) return@mapNotNull null\n        val id = part.substring(0, colonIndex).toIntOrNull() ?: return@mapNotNull null\n        val name = part.substring(colonIndex + 1)\n        CachedLiveAreaInfo(id, name)\n    }\n}\n\n/**\n * 解析直播导航排序字符串为配置列表（用于设置对话框）\n * @param orderString 逗号分隔的标识符列表，\"-\"前缀表示隐藏\n * @param cachedAreas 缓存的分区信息列表\n * @param isLoggedIn 是否已登录（决定是否包含关注项）\n * @return 直播导航项配置列表（按显示顺序）\n */\nfun parseLiveNavItemsOrderToConfig(\n    orderString: String,\n    cachedAreas: List<CachedLiveAreaInfo>,\n    isLoggedIn: Boolean\n): List<LiveNavItemConfig> {\n    if (orderString.isBlank()) {\n        // 默认配置：推荐 → (关注) → 按缓存顺序的各分区\n        return buildList {\n            add(LiveNavItemConfig(\"R\", false))\n            if (isLoggedIn) add(LiveNavItemConfig(\"F\", false))\n            cachedAreas.forEach { add(LiveNavItemConfig(it.id.toString(), false)) }\n        }\n    }\n\n    val configs = orderString.split(\",\").mapNotNull { part ->\n        val trimmed = part.trim()\n        if (trimmed.isEmpty()) return@mapNotNull null\n        val isHidden = trimmed.startsWith(\"-\")\n        val id = if (isHidden) trimmed.substring(1) else trimmed\n        if (id.isEmpty()) return@mapNotNull null\n        LiveNavItemConfig(id, isHidden)\n    }\n\n    // 收集已有的ID\n    val existingIds = configs.map { it.id }.toSet()\n\n    // 追加默认项中不存在的（新分区等）\n    val result = configs.toMutableList()\n    if (\"R\" !in existingIds) result.add(0, LiveNavItemConfig(\"R\", false))\n    if (isLoggedIn && \"F\" !in existingIds) {\n        val rIndex = result.indexOfFirst { it.id == \"R\" }\n        result.add(rIndex + 1, LiveNavItemConfig(\"F\", false))\n    }\n    // 追加缓存中有但配置中没有的新分区\n    cachedAreas.forEach { area ->\n        val areaId = area.id.toString()\n        if (areaId !in existingIds) {\n            result.add(LiveNavItemConfig(areaId, false))\n        }\n    }\n\n    // 过滤掉未登录时的\"关注\"项\n    return if (!isLoggedIn) result.filter { it.id != \"F\" } else result\n}\n\n/**\n * 获取直播导航项的显示名称\n */\nfun getLiveNavItemDisplayName(id: String, cachedAreas: List<CachedLiveAreaInfo>): String {\n    return when (id) {\n        \"R\" -> \"推荐\"\n        \"F\" -> \"关注\"\n        else -> {\n            val areaId = id.toIntOrNull()\n            cachedAreas.firstOrNull { it.id == areaId }?.name ?: \"未知分区($id)\"\n        }\n    }\n}\n\n// 用于 LiveContent 的 TopNavItem 包装\nprivate object LiveRecommendNavItemHolder : TopNavItem {\n    override fun getDisplayName(context: Context): String = \"推荐\"\n}\n\nprivate object LiveFollowingNavItemHolder : TopNavItem {\n    override fun getDisplayName(context: Context): String = \"关注\"\n}\n\nprivate data class LiveParentAreaNavItemHolder(val group: LiveAreaGroup) : TopNavItem {\n    override fun getDisplayName(context: Context): String = group.name\n}\n\n/**\n * 解析直播导航排序字符串，返回过滤后的 TopNavItem 列表（用于 LiveContent）\n * @param orderString 排序配置字符串\n * @param areaGroups 当前 API 返回的主分区列表\n * @param isLoggedIn 是否已登录\n * @return 过滤和排序后的 TopNavItem 列表\n */\nfun parseLiveNavItemsOrder(\n    orderString: String,\n    areaGroups: List<LiveAreaGroup>,\n    isLoggedIn: Boolean\n): List<TopNavItem> {\n    if (orderString.isBlank()) {\n        // 默认：推荐 → (关注) → API 返回顺序的各分区\n        return buildList {\n            add(LiveRecommendNavItemHolder)\n            if (isLoggedIn) add(LiveFollowingNavItemHolder)\n            addAll(areaGroups.map { LiveParentAreaNavItemHolder(it) })\n        }\n    }\n\n    val areaGroupMap = areaGroups.associateBy { it.id }\n    val existingIds = mutableSetOf<String>()\n\n    val result = orderString.split(\",\").mapNotNull { part ->\n        val trimmed = part.trim()\n        if (trimmed.isEmpty()) return@mapNotNull null\n        val isHidden = trimmed.startsWith(\"-\")\n        val id = if (isHidden) trimmed.substring(1) else trimmed\n        if (id.isEmpty() || isHidden) return@mapNotNull null\n        existingIds.add(id)\n        when (id) {\n            \"R\" -> LiveRecommendNavItemHolder\n            \"F\" -> if (isLoggedIn) LiveFollowingNavItemHolder else null\n            else -> {\n                val areaId = id.toIntOrNull() ?: return@mapNotNull null\n                areaGroupMap[areaId]?.let { LiveParentAreaNavItemHolder(it) }\n            }\n        }\n    }.toMutableList()\n\n    // 追加配置中没有的新分区（API 新增的）\n    areaGroups.forEach { group ->\n        if (group.id.toString() !in existingIds) {\n            result.add(LiveParentAreaNavItemHolder(group))\n        }\n    }\n\n    return result\n}\n\n/**\n * 从 TopNavItem 获取直播导航项的 ID（用于 LiveContent 恢复选中状态等）\n */\nfun getLiveNavItemId(item: TopNavItem): String? {\n    return when (item) {\n        is LiveRecommendNavItemHolder -> \"R\"\n        is LiveFollowingNavItemHolder -> \"F\"\n        is LiveParentAreaNavItemHolder -> item.group.id.toString()\n        else -> null\n    }\n}\n\n/**\n * 从 TopNavItem 获取 LiveAreaGroup（仅分区项有值）\n */\nfun getLiveNavItemAreaGroup(item: TopNavItem): LiveAreaGroup? {\n    return (item as? LiveParentAreaNavItemHolder)?.group\n}\n\n/**\n * 判断 TopNavItem 是否是直播推荐项\n */\nfun isLiveRecommendItem(item: TopNavItem): Boolean = item is LiveRecommendNavItemHolder\n\n/**\n * 判断 TopNavItem 是否是直播关注项\n */\nfun isLiveFollowingItem(item: TopNavItem): Boolean = item is LiveFollowingNavItemHolder\n\n/**\n * 判断 TopNavItem 是否是直播分区项\n */\nfun isLiveAreaItem(item: TopNavItem): Boolean = item is LiveParentAreaNavItemHolder\n\n/**\n * 获取根据设置过滤和排序后的直播导航项列表（Flow 版本）\n * 需要配合 areaGroups 使用，因此不能像 UGC/PGC 那样纯 Flow\n */\nval liveNavItemsOrderFlow: Flow<String>\n    get() = Prefs.liveNavItemsOrderFlow\n\nprivate fun <T> parseTopNavItemsOrder(orderString: String, entries: List<T>): List<T> {\n    if (orderString.isBlank()) return entries\n\n    return orderString\n        .split(\",\")\n        .mapNotNull { part ->\n            val trimmed = part.trim()\n            val isHidden = trimmed.startsWith(\"-\")\n            val actualOrdinal = trimmed.removePrefix(\"-\").toIntOrNull() ?: return@mapNotNull null\n            actualOrdinal to isHidden\n        }\n        .filter { !it.second }\n        .mapNotNull { (ordinal, _) -> entries.getOrNull(ordinal) }\n}\n\n/**\n * 获取根据设置过滤和排序后的首页导航项列表\n */\nval homeNavItemsFlow: Flow<List<HomeTopNavItem>>\n    get() = Prefs.homeNavItemsOrderFlow.map { orderString ->\n        parseHomeNavItemsOrder(orderString)\n    }\n\n/**\n * 获取根据设置过滤和排序后的 UGC 顶部导航项列表\n */\nval ugcNavItemsFlow: Flow<List<UgcTopNavItem>>\n    get() = Prefs.ugcNavItemsOrderFlow.map { orderString ->\n        parseUgcTopNavItemsOrder(orderString)\n    }\n\n/**\n * 获取根据设置过滤和排序后的 PGC 顶部导航项列表\n */\nval pgcNavItemsFlow: Flow<List<PgcTopNavItem>>\n    get() = Prefs.pgcNavItemsOrderFlow.map { orderString ->\n        parsePgcTopNavItemsOrder(orderString)\n    }\n\n/**\n * 解析导航项排序字符串\n * @param orderString 逗号分隔的 ordinal 列表，负数表示隐藏\n * @return 过滤和排序后的导航项列表\n */\nfun parseHomeNavItemsOrder(orderString: String): List<HomeTopNavItem> {\n    return parseTopNavItemsOrder(orderString, HomeTopNavItem.entries)\n}\n\nfun parseUgcTopNavItemsOrder(orderString: String): List<UgcTopNavItem> {\n    return parseTopNavItemsOrder(orderString, UgcTopNavItem.entries)\n}\n\nfun parsePgcTopNavItemsOrder(orderString: String): List<PgcTopNavItem> {\n    return parseTopNavItemsOrder(orderString, PgcTopNavItem.entries)\n}\n\n/**\n * 将指定导航项移到第一位并取消隐藏\n * 用于切换默认标签时，将新默认标签移到第一位\n * @param orderString 当前排序配置字符串\n * @param ordinal 要移到第一位的导航项 ordinal\n * @return 更新后的排序配置字符串\n */\nfun moveNavItemToFirstAndUnhide(orderString: String, ordinal: Int): String {\n    return moveNavItemToFirstAndUnhide(orderString = orderString, ordinal = ordinal, entriesCount = 0)\n}\n\n/**\n * 将指定导航项移到第一位并取消隐藏\n * @param orderString 当前排序配置字符串\n * @param ordinal 要移到第一位的导航项 ordinal\n * @param entriesCount 枚举项数量；当 orderString 为空时用于生成默认序列\n */\nfun moveNavItemToFirstAndUnhide(orderString: String, ordinal: Int, entriesCount: Int): String {\n    val normalizedOrderString = if (orderString.isBlank()) {\n        if (entriesCount <= 0) return orderString\n        (0 until entriesCount).joinToString(\",\")\n    } else {\n        orderString\n    }\n\n    val parts = normalizedOrderString.split(\",\").map { part ->\n        val trimmed = part.trim()\n        val isHidden = trimmed.startsWith(\"-\")\n        val absNum = trimmed.removePrefix(\"-\").toIntOrNull() ?: return@map 0 to false\n        absNum to isHidden\n    }\n\n    // 找到目标项\n    val targetItem = parts.find { it.first == ordinal }\n    if (targetItem == null) return normalizedOrderString\n\n    // 构建新的顺序：目标项在前，其他项按原顺序在后\n    val otherItems = parts.filter { it.first != ordinal }\n    val newParts = listOf(\n        ordinal.toString()  // 默认标签在第一位，取消隐藏\n    ) + otherItems.map { (ord, hidden) ->\n        if (hidden) \"-$ord\" else \"$ord\"\n    }\n\n    return newParts.joinToString(\",\")\n}\n\n/**\n * 解析排序字符串为配置列表\n * @param orderString 逗号分隔的 ordinal 列表，负数表示隐藏\n * @return 导航项配置列表（按显示顺序）\n */\nfun parseNavItemsOrderToConfig(orderString: String): List<NavItemConfig> {\n    return parseNavItemsOrderToConfig(orderString, HomeTopNavItem.entries.size)\n}\n\n/**\n * 解析排序字符串为配置列表（通用版本）\n * @param orderString 逗号分隔的 ordinal 列表，负数表示隐藏\n * @param entriesCount 枚举项数量\n */\nfun parseNavItemsOrderToConfig(orderString: String, entriesCount: Int): List<NavItemConfig> {\n    if (entriesCount <= 0) return emptyList()\n\n    if (orderString.isBlank()) {\n        return (0 until entriesCount).map { NavItemConfig(it, false) }\n    }\n\n    return orderString\n        .split(\",\")\n        .mapNotNull { part ->\n            val trimmed = part.trim()\n            val isHidden = trimmed.startsWith(\"-\")\n            val actualOrdinal = trimmed.removePrefix(\"-\").toIntOrNull() ?: return@mapNotNull null\n            if (actualOrdinal !in 0 until entriesCount) return@mapNotNull null\n            NavItemConfig(actualOrdinal, isHidden)\n        }\n}\n\n// ======================== 主导航（左侧侧栏）配置 ========================\n\n/**\n * 可配置的主导航项列表（不含 User 和 Settings）\n */\nval configurableDrawerItems = listOf(\n    DrawerItem.Search,\n    DrawerItem.Home,\n    DrawerItem.UGC,\n    DrawerItem.PGC,\n    DrawerItem.Live\n)\n\n/**\n * 解析主导航排序字符串为 DrawerItem 列表（过滤隐藏项）\n */\nfun parseDrawerNavItemsOrder(orderString: String): List<DrawerItem> {\n    if (orderString.isBlank()) return configurableDrawerItems\n\n    return orderString\n        .split(\",\")\n        .mapNotNull { part ->\n            val trimmed = part.trim()\n            val isHidden = trimmed.startsWith(\"-\")\n            if (isHidden) return@mapNotNull null\n            val ordinal = trimmed.toIntOrNull() ?: return@mapNotNull null\n            DrawerItem.entries.getOrNull(ordinal)\n        }\n        .filter { it in configurableDrawerItems }\n}\n\n/**\n * 解析主导航排序字符串为配置列表（用于设置对话框）\n */\nfun parseDrawerNavItemsOrderToConfig(orderString: String): List<NavItemConfig> {\n    if (orderString.isBlank()) {\n        return configurableDrawerItems.mapIndexed { _, item ->\n            NavItemConfig(item.ordinal, false)\n        }\n    }\n\n    val configs = orderString\n        .split(\",\")\n        .mapNotNull { part ->\n            val trimmed = part.trim()\n            val isHidden = trimmed.startsWith(\"-\")\n            val ordinal = trimmed.removePrefix(\"-\").toIntOrNull() ?: return@mapNotNull null\n            val drawerItem = DrawerItem.entries.getOrNull(ordinal) ?: return@mapNotNull null\n            if (drawerItem !in configurableDrawerItems) return@mapNotNull null\n            NavItemConfig(ordinal, isHidden)\n        }\n\n    // 追加配置中缺失的可配置项\n    val existingOrdinals = configs.map { it.ordinal }.toSet()\n    val missingConfigs = configurableDrawerItems\n        .filter { it.ordinal !in existingOrdinals }\n        .map { NavItemConfig(it.ordinal, false) }\n\n    return configs + missingConfigs\n}\n\n/**\n * 保存主导航排序配置\n */\nfun saveDrawerNavConfigs(navConfigs: List<NavItemConfig>) {\n    val finalOrderString = navConfigs.joinToString(\",\") { config ->\n        if (config.hidden) \"-${config.ordinal}\" else \"${config.ordinal}\"\n    }\n    Prefs.drawerNavItemsOrder = finalOrderString\n}\n\n/**\n * 获取根据设置过滤和排序后的主导航项列表（Flow 版本）\n */\nval drawerNavItemsFlow: Flow<List<DrawerItem>>\n    get() = Prefs.drawerNavItemsOrderFlow.map { orderString ->\n        parseDrawerNavItemsOrder(orderString)\n    }\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/util/PlayerActivityUtil.kt",
    "content": "package dev.aaa1115910.bv.tv.util\n\nimport android.content.Context\nimport dev.aaa1115910.bv.entity.proxy.ProxyArea\nimport dev.aaa1115910.bv.tv.activities.video.RemoteControllerPanelDemoActivity\nimport dev.aaa1115910.bv.tv.activities.video.VideoPlayerV3Activity\nimport dev.aaa1115910.bv.util.Prefs\n\nfun launchPlayerActivity(\n    context: Context,\n    avid: Long,\n    cid: Long,\n    title: String,\n    partTitle: String,\n    played: Int,\n    fromSeason: Boolean,\n    subType: Int? = null,\n    epid: Int? = null,\n    seasonId: Int? = null,\n    isVerticalVideo: Boolean = false,\n    proxyArea: ProxyArea = ProxyArea.MainLand,\n    playerIconIdle: String = \"\",\n    playerIconMoving: String = \"\",\n    play: Long = 0,\n    danmaku: Int = 0,\n    like: Int = 0,\n    coin: Int = 0,\n    favorite: Int = 0,\n    upName: String = \"\",\n    upId: Long = 0L,\n    upFace: String = \"\",\n    pubTime: String = \"\"\n) {\n    if (Prefs.showedRemoteControllerPanelDemo) {\n        VideoPlayerV3Activity.actionStart(\n            context, avid, cid, title, partTitle, played, fromSeason, subType, epid, seasonId,\n            isVerticalVideo, proxyArea, playerIconIdle, playerIconMoving,\n            play, danmaku, like, coin, favorite, upName, upId, upFace, pubTime\n        )\n    } else {\n        RemoteControllerPanelDemoActivity.actionStart(\n            context, avid, cid, title, partTitle, played, fromSeason, subType, epid, seasonId,\n            isVerticalVideo, proxyArea, playerIconIdle, playerIconMoving,\n            play, danmaku, like, coin, favorite, upName, upId, upFace, pubTime\n        )\n    }\n}"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/util/ProvideListBringIntoViewSpec.kt",
    "content": "package dev.aaa1115910.bv.tv.util\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.gestures.BringIntoViewSpec\nimport androidx.compose.foundation.gestures.LocalBringIntoViewSpec\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlin.math.abs\n\n/**\n * Provides a [BringIntoViewSpec] that calculates the scroll offset for a child item in a LazyList\n * with intelligent positioning logic.\n *\n * The positioning logic:\n * 1. If the focused element is fully visible, don't scroll\n * 2. If the focused element is in the upper, align its top edge with container top\n * 3. If the focused element is in the lower, align its bottom edge with container bottom\n *\n * @param padding 容器上下左右预留的内边距。单位是dp\n *     注意：延迟列表默认只组合可见项，必须留边距露出一点点下一行用来确保将要获得焦点的项已被组合，否则下移的时候焦点会选中下一行的第一个，上移的时候焦点会选中上一行的最后一个（焦点乱跳的问题）\n *     另外，本应用列表用的视频卡片组件有发光效果，不留边距会没显示不全。\n * @param topPadding 容器上边距。默认与 [padding] 相同\n * @param bottomPadding 容器下边距。默认与 [padding] 相同\n * @param content 包含在 LazyList 中的内容\n */\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun ProvideListBringIntoViewSpec(\n    padding: Dp = 24.dp,\n    topPadding: Dp = padding,\n    bottomPadding: Dp = padding,\n    content: @Composable () -> Unit,\n) {\n    val density = LocalDensity.current\n    val topPaddingPx = remember(topPadding, density) { with(density) { topPadding.toPx() } }\n    val bottomPaddingPx = remember(bottomPadding, density) { with(density) { bottomPadding.toPx() } }\n    val bringIntoViewSpec = remember(topPaddingPx, bottomPaddingPx) {\n        object : BringIntoViewSpec {\n            override fun calculateScrollDistance(\n                offset: Float,\n                size: Float,\n                containerSize: Float\n            ): Float = calculateScrollDistanceWithPadding(\n                offset = offset,\n                size = size,\n                containerSize = containerSize,\n                topPadding = topPaddingPx,\n                bottomPadding = bottomPaddingPx,\n            )\n        }\n    }\n    CompositionLocalProvider(\n        LocalBringIntoViewSpec provides bringIntoViewSpec,\n        content = content,\n    )\n}\n\nprivate fun calculateScrollDistanceWithPadding(\n    offset: Float,\n    size: Float,\n    containerSize: Float,\n    topPadding: Float, // 容器上边距\n    bottomPadding: Float, // 容器下边距\n): Float {\n    val trailingEdge = offset + size + bottomPadding\n    val leadingEdge = offset - topPadding\n    return when {\n\n        // 如果组件已经完整显示，不滚动\n        leadingEdge >= 0 && trailingEdge <= containerSize -> 0f\n\n        // 如果组件可见但比容器大，不滚动\n        leadingEdge < 0 && trailingEdge > containerSize -> 0f\n\n        // 找出使其中一条边与容器的边重合所需的最小滚动量\n        abs(leadingEdge) < abs(trailingEdge - containerSize) -> leadingEdge\n        else -> trailingEdge - containerSize\n    }\n}\n"
  },
  {
    "path": "app/tv/src/main/kotlin/dev/aaa1115910/bv/tv/util/TvLazyListFocusRestorer.kt",
    "content": "package dev.aaa1115910.bv.tv.util\n\nimport android.view.KeyEvent\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport dev.aaa1115910.bv.entity.carddata.VideoCardData\n\n@Stable\nclass TvLazyListFocusRestorer internal constructor(\n    val fallbackFocusRequester: FocusRequester\n) {\n    fun containerModifier(modifier: Modifier = Modifier): Modifier {\n        return modifier.focusRestorer(fallbackFocusRequester)\n    }\n\n    fun firstItemModifier(index: Int, modifier: Modifier = Modifier): Modifier {\n        return if (index == 0) {\n            modifier.focusRequester(fallbackFocusRequester)\n        } else {\n            modifier\n        }\n    }\n}\n\n@Composable\nfun rememberTvLazyListFocusRestorer(\n    fallbackFocusRequester: FocusRequester = remember { FocusRequester() }\n): TvLazyListFocusRestorer {\n    return remember(fallbackFocusRequester) {\n        TvLazyListFocusRestorer(fallbackFocusRequester)\n    }\n}\n\nfun VideoCardData.stableItemKey(): Any {\n    return when {\n        seasonId != null -> \"season-$seasonId-${epId ?: 0}-$upId\"\n        avid > 0 -> \"av-$avid-$upId\"\n        else -> \"$title|$upId\"\n    }\n}\n\nfun Modifier.blockDownFocusExitAtGridEnd(\n    currentIndex: Int,\n    itemCount: Int,\n    columnCount: Int\n): Modifier {\n    return onPreviewKeyEvent { event ->\n        val nativeEvent = event.nativeKeyEvent\n        if (nativeEvent.keyCode != KeyEvent.KEYCODE_DPAD_DOWN) return@onPreviewKeyEvent false\n        if (nativeEvent.action != KeyEvent.ACTION_DOWN && nativeEvent.action != KeyEvent.ACTION_UP) {\n            return@onPreviewKeyEvent false\n        }\n\n        val hasNextRow = itemCount > 0 && currentIndex >= 0 && currentIndex + columnCount < itemCount\n        !hasNextRow\n    }\n}"
  },
  {
    "path": "app/tv/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!--视频 Grid 中上下左右间距-->\n    <dimen name=\"grid_padding\">16dp</dimen>\n\n    <!--视频 Grid 内部间距-->\n    <dimen name=\"grid_spacedBy\">13dp</dimen>\n\n</resources>"
  },
  {
    "path": "app/tv/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"title_activity_anime_timeline\">番剧放送时间表</string>\n    <string name=\"title_activity_favorite\">个人收藏</string>\n    <string name=\"title_activity_follow\">已关注</string>\n    <string name=\"title_activity_following_season\">正在追</string>\n    <string name=\"title_activity_history\">历史记录</string>\n    <string name=\"title_activity_login\">登录</string>\n    <string name=\"title_activity_logs\">日志列表</string>\n    <string name=\"title_activity_media_codec\">解码器信息</string>\n    <string name=\"title_activity_pgc_index\">PGC 索引</string>\n    <string name=\"title_activity_remote_controller_panel_demo\">遥控板按键演示</string>\n    <string name=\"title_activity_search_input\">搜索输入</string>\n    <string name=\"title_activity_search_result\">搜索结果</string>\n    <string name=\"title_activity_season_info\">剧集信息</string>\n    <string name=\"title_activity_settings\">设置</string>\n    <string name=\"title_activity_speed_test\">网络测速</string>\n    <string name=\"title_activity_tag\">视频标签</string>\n    <string name=\"title_activity_toview\">现在不看</string>\n    <string name=\"title_activity_up_info\">UP 投稿</string>\n    <string name=\"title_activity_user_info\">用户信息</string>\n    <string name=\"title_activity_user_lock_settings\">用户锁设置</string>\n    <string name=\"title_activity_user_switch\">用户切换</string>\n    <string name=\"title_activity_video_info\">视频信息</string>\n    <string name=\"title_activity_video_player_v3\">视频播放</string>\n    <string name=\"entry_follow_screen\">在列表区域按菜单键打开已关注UP列表</string>\n</resources>"
  },
  {
    "path": "app/tv/src/main/res/values/themes.xml",
    "content": "<resources>\n\n    <style name=\"Theme.BV.TV.Splash\" parent=\"Theme.SplashScreen\">\n        <item name=\"windowSplashScreenBackground\">@android:color/black</item>\n        <item name=\"postSplashScreenTheme\">@style/Theme.BV.TV</item>\n        <item name=\"windowSplashScreenAnimatedIcon\">@drawable/ic_launcher_foreground</item>\n    </style>\n\n    <style name=\"Theme.BV.TV\" parent=\"Theme.Material3.DayNight.NoActionBar\">\n        <item name=\"android:windowBackground\">@android:color/black</item>\n    </style>\n</resources>"
  },
  {
    "path": "bili-api/.gitignore",
    "content": "/build"
  },
  {
    "path": "bili-api/build.gradle.kts",
    "content": "plugins {\n    alias(gradleLibs.plugins.google.ksp)\n    alias(gradleLibs.plugins.kotlin.jvm)\n    alias(gradleLibs.plugins.kotlin.serialization)\n}\n\ngroup = \"dev.aaa1115910\"\n\ndependencies {\n    ksp(libs.koin.ksp.compiler)\n    implementation(project(\":bili-api:grpc\"))\n    implementation(libs.koin.core)\n    implementation(libs.koin.annotations)\n    implementation(libs.jsoup)\n    implementation(libs.brotli)\n    implementation(libs.kotlinx.coroutines)\n    implementation(libs.kotlinx.serialization)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.encoding)\n    //implementation(libs.ktor.jsoup)\n    implementation(libs.ktor.client.okhttp)\n    implementation(libs.ktor.client.serialization.kotlinx)\n    implementation(libs.logging)\n    implementation(libs.slf4j.simple)\n    testImplementation(libs.kotlin.test)\n}\n\ntasks.test {\n    useJUnitPlatform()\n}"
  },
  {
    "path": "bili-api/example-response/live-event/COMBO_SEND.json5",
    "content": "{\n  \"cmd\": \"COMBO_SEND\",\n  \"data\": {\n    \"action\": \"投喂\",\n    \"batch_combo_id\": \"batch:gift:combo_id:21486712:168598:31036:1669433933.0434\",\n    \"batch_combo_num\": 2,\n    \"combo_id\": \"gift:combo_id:21486712:168598:31036:1669433933.0427\",\n    \"combo_num\": 2,\n    \"combo_total_coin\": 200,\n    \"dmscore\": 56,\n    \"gift_id\": 31036,\n    \"gift_name\": \"小花花\",\n    \"gift_num\": 0,\n    \"is_naming\": false,\n    \"is_show\": 1,\n    \"medal_info\": {\n      \"anchor_roomid\": 0,\n      \"anchor_uname\": \"\",\n      \"guard_level\": 0,\n      \"icon_id\": 0,\n      \"is_lighted\": 1,\n      \"medal_color\": 9272486,\n      \"medal_color_border\": 9272486,\n      \"medal_color_end\": 9272486,\n      \"medal_color_start\": 9272486,\n      \"medal_level\": 11,\n      \"medal_name\": \"刺儿\",\n      \"special\": \"\",\n      \"target_id\": 168598\n    },\n    \"name_color\": \"\",\n    \"r_uname\": \"逍遥散人\",\n    \"ruid\": 168598,\n    \"send_master\": null,\n    \"total_num\": 2,\n    \"uid\": 21486712,\n    \"uname\": \"Ms星鸢\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/DANMU_MSG.json5",
    "content": "{\n  \"cmd\": \"DANMU_MSG\",\n  \"info\": [\n    [\n      0,\n      1,\n      25,\n      5816798,\n      1669371202340,\n      -338894660,\n      0,\n      \"987fef1e\",\n      0,\n      0,\n      0,\n      \"\",\n      0,\n      \"{}\",\n      \"{}\",\n      {\n        \"mode\": 0,\n        \"show_player_type\": 0,\n        \"extra\": \"{\\\"send_from_me\\\":false,\\\"mode\\\":0,\\\"color\\\":5816798,\\\"dm_type\\\":0,\\\"font_size\\\":25,\\\"player_mode\\\":1,\\\"show_player_type\\\":0,\\\"content\\\":\\\"遗物不卖有啥用？走文化吗？\\\",\\\"user_hash\\\":\\\"2558521118\\\",\\\"emoticon_unique\\\":\\\"\\\",\\\"bulge_display\\\":0,\\\"recommend_score\\\":1,\\\"main_state_dm_color\\\":\\\"\\\",\\\"objective_state_dm_color\\\":\\\"\\\",\\\"direction\\\":0,\\\"pk_direction\\\":0,\\\"quartet_direction\\\":0,\\\"anniversary_crowd\\\":0,\\\"yeah_space_type\\\":\\\"\\\",\\\"yeah_space_url\\\":\\\"\\\",\\\"jump_to_url\\\":\\\"\\\",\\\"space_type\\\":\\\"\\\",\\\"space_url\\\":\\\"\\\"}\"\n      },\n      {\n        \"activity_identity\": \"\",\n        \"activity_source\": 0,\n        \"not_show\": 0\n      }\n    ],\n    //弹幕内容\n    \"遗物不卖有啥用？走文化吗？\",\n    [\n      //mid\n      39344889,\n      //用户名\n      \"风的节奏_疾风\",\n      0,\n      0,\n      0,\n      10000,\n      1,\n      \"\"\n    ],\n    //粉丝勋章 未佩戴为[]\n    [\n      14,\n      //粉丝勋章等级\n      \"子轩\",\n      //粉丝勋章名称\n      \"战术级子轩\",\n      13355,\n      12478086,\n      \"\",\n      0,\n      12478086,\n      12478086,\n      12478086,\n      0,\n      1,\n      3519797\n    ],\n    [\n      15,\n      0,\n      6406234,\n      \"\\u003e50000\",\n      0\n    ],\n    [\n      \"\",\n      \"\"\n    ],\n    0,\n    0,\n    null,\n    {\n      \"ts\": 1669371202,\n      \"ct\": \"347A3E92\"\n    },\n    0,\n    0,\n    null,\n    null,\n    0,\n    42\n  ]\n}"
  },
  {
    "path": "bili-api/example-response/live-event/ENTRY_EFFECT.json5",
    "content": "{\n  \"cmd\": \"ENTRY_EFFECT\",\n  \"data\": {\n    \"id\": 4,\n    \"uid\": 12342661,\n    \"target_id\": 8739477,\n    \"mock_effect\": 0,\n    \"face\": \"https://i1.hdslb.com/bfs/face/3d876e4fb959d0089e3c0299afae9fead128c70a.jpg\",\n    \"privilege_type\": 3,\n    \"copy_writing\": \"欢迎舰长 <%整点薯条吃吃吃%> 进入直播间\",\n    \"copy_color\": \"#ffffff\",\n    \"highlight_color\": \"#E6FF00\",\n    \"priority\": 1,\n    \"basemap_url\": \"https://i0.hdslb.com/bfs/live/mlive/11a6e8eb061c3e715d0a6a2ac0ddea2faa15c15e.png\",\n    \"show_avatar\": 1,\n    \"effective_time\": 2,\n    \"web_basemap_url\": \"https://i0.hdslb.com/bfs/live/mlive/11a6e8eb061c3e715d0a6a2ac0ddea2faa15c15e.png\",\n    \"web_effective_time\": 2,\n    \"web_effect_close\": 0,\n    \"web_close_time\": 0,\n    \"business\": 1,\n    \"copy_writing_v2\": \"欢迎舰长 <%整点薯条吃吃吃%> 进入直播间\",\n    \"icon_list\": [],\n    \"max_delay_time\": 7,\n    \"trigger_time\": 1669400294516525185,\n    \"identities\": 6,\n    \"effect_silent_time\": 0,\n    \"effective_time_new\": 0,\n    \"web_dynamic_url_webp\": \"\",\n    \"web_dynamic_url_apng\": \"\",\n    \"mobile_dynamic_url_webp\": \"\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/GUARD_BUY.json5",
    "content": "{\n  \"cmd\": \"GUARD_BUY\",\n  \"data\": {\n    \"uid\": 6501027,\n    \"username\": \"玉紫\",\n    \"guard_level\": 3,\n    \"num\": 1,\n    \"price\": 198000,\n    \"gift_id\": 10003,\n    \"gift_name\": \"舰长\",\n    \"start_time\": 1669435051,\n    \"end_time\": 1669435051\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/HOT_RANK_CHANGED.json5",
    "content": "{\n  \"cmd\": \"HOT_RANK_CHANGED\",\n  \"data\": {\n    \"rank\": 8,\n    \"trend\": 1,\n    \"countdown\": 1795,\n    \"timestamp\": 1669401005,\n    \"web_url\": \"https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=2&area_id=2&parent_area_id=2&second_area_id=0\",\n    \"live_url\": \"https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=1&area_id=2&parent_area_id=2&second_area_id=0&is_live_half_webview=1&hybrid_rotate_d=1&hybrid_half_ui=1,3,100p,70p,ffffff,0,30,100,12,0;2,2,375,100p,ffffff,0,30,100,0,0;3,3,100p,70p,ffffff,0,30,100,12,0;4,2,375,100p,ffffff,0,30,100,0,0;5,3,100p,70p,ffffff,0,30,100,0,0;6,3,100p,70p,ffffff,0,30,100,0,0;7,3,100p,70p,ffffff,0,30,100,0,0;8,3,100p,70p,ffffff,0,30,100,0,0\",\n    \"blink_url\": \"https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=3&area_id=2&parent_area_id=2&second_area_id=0&is_live_half_webview=1&hybrid_rotate_d=1&is_cling_player=1&hybrid_half_ui=1,3,100p,70p,ffffff,0,30,100,0,0;2,2,375,100p,ffffff,0,30,100,0,0;3,3,100p,70p,ffffff,0,30,100,0,0;4,2,375,100p,ffffff,0,30,100,0,0;5,3,100p,70p,ffffff,0,30,100,0,0;6,3,100p,70p,ffffff,0,30,100,0,0;7,3,100p,70p,ffffff,0,30,100,0,0;8,3,100p,70p,ffffff,0,30,100,0,0\",\n    \"live_link_url\": \"https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=5&area_id=2&parent_area_id=2&second_area_id=0&is_live_half_webview=1&hybrid_rotate_d=1&is_cling_player=1&hybrid_half_ui=1,3,100p,70p,f4eefa,0,30,100,0,0;2,2,375,100p,f4eefa,0,30,100,0,0;3,3,100p,70p,f4eefa,0,30,100,0,0;4,2,375,100p,f4eefa,0,30,100,0,0;5,3,100p,70p,f4eefa,0,30,100,0,0;6,3,100p,70p,f4eefa,0,30,100,0,0;7,3,100p,70p,f4eefa,0,30,100,0,0;8,3,100p,70p,f4eefa,0,30,100,0,0\",\n    \"pc_link_url\": \"https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=4&is_live_half_webview=1&area_id=2&parent_area_id=2&second_area_id=0&pc_ui=338,465,f4eefa,0\",\n    \"icon\": \"https://i0.hdslb.com/bfs/live/65dbe013f7379c78fc50dfb2fd38d67f5e4895f9.png\",\n    \"area_name\": \"网游\",\n    \"rank_desc\": \"\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/HOT_RANK_CHANGED_V2.json5",
    "content": "{\n  \"cmd\": \"HOT_RANK_CHANGED_V2\",\n  \"data\": {\n    \"rank\": 15,\n    \"trend\": 0,\n    \"countdown\": 1140,\n    \"timestamp\": 1669399860,\n    \"web_url\": \"https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=2&area_id=1&parent_area_id=1&second_area_id=21\",\n    \"live_url\": \"https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=1&area_id=1&parent_area_id=1&second_area_id=21&is_live_half_webview=1&hybrid_rotate_d=1&hybrid_half_ui=1,3,100p,70p,ffffff,0,30,100,12,0;2,2,375,100p,ffffff,0,30,100,0,0;3,3,100p,70p,ffffff,0,30,100,12,0;4,2,375,100p,ffffff,0,30,100,0,0;5,3,100p,70p,ffffff,0,30,100,0,0;6,3,100p,70p,ffffff,0,30,100,0,0;7,3,100p,70p,ffffff,0,30,100,0,0;8,3,100p,70p,ffffff,0,30,100,0,0\",\n    \"blink_url\": \"https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=3&area_id=1&parent_area_id=1&second_area_id=21&is_live_half_webview=1&hybrid_rotate_d=1&is_cling_player=1&hybrid_half_ui=1,3,100p,70p,ffffff,0,30,100,0,0;2,2,375,100p,ffffff,0,30,100,0,0;3,3,100p,70p,ffffff,0,30,100,0,0;4,2,375,100p,ffffff,0,30,100,0,0;5,3,100p,70p,ffffff,0,30,100,0,0;6,3,100p,70p,ffffff,0,30,100,0,0;7,3,100p,70p,ffffff,0,30,100,0,0;8,3,100p,70p,ffffff,0,30,100,0,0\",\n    \"live_link_url\": \"https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=5&area_id=1&parent_area_id=1&second_area_id=21&is_live_half_webview=1&hybrid_rotate_d=1&is_cling_player=1&hybrid_half_ui=1,3,100p,70p,f4eefa,0,30,100,0,0;2,2,375,100p,f4eefa,0,30,100,0,0;3,3,100p,70p,f4eefa,0,30,100,0,0;4,2,375,100p,f4eefa,0,30,100,0,0;5,3,100p,70p,f4eefa,0,30,100,0,0;6,3,100p,70p,f4eefa,0,30,100,0,0;7,3,100p,70p,f4eefa,0,30,100,0,0;8,3,100p,70p,f4eefa,0,30,100,0,0\",\n    \"pc_link_url\": \"https://live.bilibili.com/p/html/live-app-hotrank/index.html?clientType=4&is_live_half_webview=1&area_id=1&parent_area_id=1&second_area_id=21&pc_ui=338,465,f4eefa,0\",\n    \"icon\": \"https://i0.hdslb.com/bfs/live/cb2e160ac4f562b347bb5ae6e635688ebc69580f.png\",\n    \"area_name\": \"视频唱见\",\n    \"rank_desc\": \"视频唱见top50\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/HOT_RANK_SETTLEMENT.json5",
    "content": "{\n  \"cmd\": \"HOT_RANK_SETTLEMENT\",\n  \"data\": {\n    \"area_name\": \"手游\",\n    \"cache_key\": \"c2f8a79cc9a709237fb65df23fd61025\",\n    \"dm_msg\": \"恭喜主播 <% 逍遥散人 %> 荣登限时热门榜手游榜top9! 即将获得轮播资源位推荐哦！\",\n    \"dmscore\": 144,\n    \"face\": \"https://i1.hdslb.com/bfs/face/8a5de2d7486251e80307d8600cbf8649eb4035fe.jpg\",\n    \"icon\": \"https://i0.hdslb.com/bfs/live/b4961bcfba56a26b69c35690dfcbdabbeb973c64.png\",\n    \"rank\": 9,\n    \"timestamp\": 1669435204,\n    \"uname\": \"逍遥散人\",\n    \"url\": \"https://live.bilibili.com/p/html/live-app-hotrank/result.html?is_live_half_webview=1&hybrid_half_ui=1,5,250,200,f4eefa,0,30,0,0,0;2,5,250,200,f4eefa,0,30,0,0,0;3,5,250,200,f4eefa,0,30,0,0,0;4,5,250,200,f4eefa,0,30,0,0,0;5,5,250,200,f4eefa,0,30,0,0,0;6,5,250,200,f4eefa,0,30,0,0,0;7,5,250,200,f4eefa,0,30,0,0,0;8,5,250,200,f4eefa,0,30,0,0,0&areaId=3&cache_key=c2f8a79cc9a709237fb65df23fd61025\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/HOT_RANK_SETTLEMENT_V2.json5",
    "content": "{\n  \"cmd\": \"HOT_RANK_SETTLEMENT_V2\",\n  \"data\": {\n    \"rank\": 7,\n    \"uname\": \"逍遥散人\",\n    \"face\": \"https://i1.hdslb.com/bfs/face/8a5de2d7486251e80307d8600cbf8649eb4035fe.jpg\",\n    \"timestamp\": 1669434904,\n    \"icon\": \"https://i0.hdslb.com/bfs/live/cb2e160ac4f562b347bb5ae6e635688ebc69580f.png\",\n    \"area_name\": \"原神\",\n    \"url\": \"https://live.bilibili.com/p/html/live-app-hotrank/result.html?is_live_half_webview=1&hybrid_half_ui=1,5,250,200,f4eefa,0,30,0,0,0;2,5,250,200,f4eefa,0,30,0,0,0;3,5,250,200,f4eefa,0,30,0,0,0;4,5,250,200,f4eefa,0,30,0,0,0;5,5,250,200,f4eefa,0,30,0,0,0;6,5,250,200,f4eefa,0,30,0,0,0;7,5,250,200,f4eefa,0,30,0,0,0;8,5,250,200,f4eefa,0,30,0,0,0&areaId=321&cache_key=17276a47cc6fd1420feb0f2d86f60e85\",\n    \"cache_key\": \"17276a47cc6fd1420feb0f2d86f60e85\",\n    \"dm_msg\": \"恭喜主播 <% 逍遥散人 %> 荣登限时热门榜原神榜top7! 即将获得轮播资源位推荐哦！\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/HOT_ROOM_NOTIFY.json5",
    "content": "{\n  \"cmd\": \"HOT_ROOM_NOTIFY\",\n  \"data\": {\n    \"threshold\": 10000,\n    \"ttl\": 300,\n    \"exit_no_refresh\": 1,\n    \"random_delay_req_v2\": [\n      {\n        \"path\": \"/live/getRoundPlayVideo\",\n        \"delay\": 10\n      },\n      {\n        \"path\": \"/xlive/web-room/v1/index/getOffLiveList\",\n        \"delay\": 120000\n      }\n    ]\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/INTERACT_WORD.json5",
    "content": "{\n  \"cmd\": \"INTERACT_WORD\",\n  \"data\": {\n    \"contribution\": {\n      \"grade\": 0\n    },\n    \"dmscore\": 2,\n    \"fans_medal\": {\n      \"anchor_roomid\": 0,\n      \"guard_level\": 0,\n      \"icon_id\": 0,\n      \"is_lighted\": 0,\n      \"medal_color\": 0,\n      \"medal_color_border\": 0,\n      \"medal_color_end\": 0,\n      \"medal_color_start\": 0,\n      \"medal_level\": 0,\n      \"medal_name\": \"\",\n      \"score\": 0,\n      \"special\": \"\",\n      \"target_id\": 0\n    },\n    \"identities\": [\n      1\n    ],\n    \"is_spread\": 0,\n    \"msg_type\": 1,\n    \"privilege_type\": 0,\n    \"roomid\": 21721813,\n    \"score\": 1669399871533,\n    \"spread_desc\": \"\",\n    \"spread_info\": \"\",\n    \"tail_icon\": 0,\n    \"timestamp\": 1669399871,\n    \"trigger_time\": 1669399870422980000,\n    \"uid\": 268831673,\n    \"uname\": \"丸子泡汤ON\",\n    \"uname_color\": \"\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/LIKE_INFO_V3_CLICK.json5",
    "content": "{\n  \"cmd\": \"LIKE_INFO_V3_CLICK\",\n  \"data\": {\n    \"show_area\": 0,\n    \"msg_type\": 6,\n    \"like_icon\": \"https://i0.hdslb.com/bfs/live/23678e3d90402bea6a65251b3e728044c21b1f0f.png\",\n    \"uid\": 393552291,\n    \"like_text\": \"为主播点赞了\",\n    \"uname\": \"有卢克迪丽歇斯\",\n    \"uname_color\": \"\",\n    \"identities\": [\n      1\n    ],\n    \"fans_medal\": {\n      \"target_id\": 0,\n      \"medal_level\": 0,\n      \"medal_name\": \"\",\n      \"medal_color\": 0,\n      \"medal_color_start\": 12632256,\n      \"medal_color_end\": 12632256,\n      \"medal_color_border\": 12632256,\n      \"is_lighted\": 0,\n      \"guard_level\": 0,\n      \"special\": \"\",\n      \"icon_id\": 0,\n      \"anchor_roomid\": 0,\n      \"score\": 0\n    },\n    \"contribution_info\": {\n      \"grade\": 0\n    },\n    \"dmscore\": 20\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/LIKE_INFO_V3_UPDATE.json5",
    "content": "{\n  \"cmd\": \"LIKE_INFO_V3_UPDATE\",\n  \"data\": {\n    \"click_count\": 15805\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/LIVE_INTERACTIVE_GAME.json5",
    "content": "{\n  \"cmd\": \"LIVE_INTERACTIVE_GAME\",\n  \"data\": {\n    \"type\": 2,\n    \"uid\": 2588066,\n    \"uname\": \"傻傻分不清噜\",\n    \"uface\": \"\",\n    \"gift_id\": 0,\n    \"gift_name\": \"\",\n    \"gift_num\": 0,\n    \"price\": 0,\n    \"paid\": false,\n    \"msg\": \"这个debuff还行不影响\",\n    \"fans_medal_level\": 0,\n    \"guard_level\": 0,\n    \"timestamp\": 1669400655,\n    \"anchor_lottery\": null,\n    \"pk_info\": null,\n    \"anchor_info\": null,\n    \"combo_info\": null\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/LIVE_MULTI_VIEW_CHANGE.json5",
    "content": "{\n  \"cmd\": \"LIVE_MULTI_VIEW_CHANGE\",\n  \"data\": {\n    \"scatter\": {\n      \"max\": 120,\n      \"min\": 5\n    }\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/NOTICE_MSG.json5",
    "content": "{\n  \"cmd\": \"NOTICE_MSG\",\n  \"id\": 712,\n  \"name\": \"地区争-任务100元\",\n  \"full\": {\n    \"head_icon\": \"https://i0.hdslb.com/bfs/live/ab106f494f4cc0c94fb78ed46144c72f6db000f6.webp\",\n    \"tail_icon\": \"https://i0.hdslb.com/bfs/live/822da481fdaba986d738db5d8fd469ffa95a8fa1.webp\",\n    \"head_icon_fa\": \"https://i0.hdslb.com/bfs/live/ab106f494f4cc0c94fb78ed46144c72f6db000f6.webp\",\n    \"tail_icon_fa\": \"https://i0.hdslb.com/bfs/live/38cb2a9f1209b16c0f15162b0b553e3b28d9f16f.png\",\n    \"head_icon_fan\": 1,\n    \"tail_icon_fan\": 4,\n    \"background\": \"#b6272b\",\n    \"color\": \"#FFFFFFFF\",\n    \"highlight\": \"#FDFF2FFF\",\n    \"time\": 15\n  },\n  \"half\": {\n    \"head_icon\": \"https://i0.hdslb.com/bfs/live/ab106f494f4cc0c94fb78ed46144c72f6db000f6.webp\",\n    \"tail_icon\": \"\",\n    \"background\": \"#b6272b\",\n    \"color\": \"#FFFFFFFF\",\n    \"highlight\": \"#FDFF2FFF\",\n    \"time\": 15\n  },\n  \"side\": {\n    \"head_icon\": \"\",\n    \"background\": \"\",\n    \"color\": \"\",\n    \"highlight\": \"\",\n    \"border\": \"\"\n  },\n  \"roomid\": 25906864,\n  \"real_roomid\": 25906864,\n  \"msg_common\": \"恭喜<%酥软奶甜-满月快乐%>完成BLS年终决选赛任务，直播间发放价值100元的红包，快来抢鸭！\",\n  \"msg_self\": \"恭喜<%酥软奶甜-满月快乐%>完成BLS年终决选赛任务，直播间发放价值100元的红包，快来抢鸭！\",\n  \"link_url\": \"https://live.bilibili.com/25906864?broadcast_type=1&is_room_feed=1&from=28003&extra_jump_from=28003&live_lottery_type=1\",\n  \"msg_type\": 2,\n  \"shield_uid\": -1,\n  \"business_id\": \"106\",\n  \"scatter\": {\n    \"min\": 0,\n    \"max\": 0\n  },\n  \"marquee_id\": \"\",\n  \"notice_type\": 0\n}"
  },
  {
    "path": "bili-api/example-response/live-event/ONLINE_RANK_COUNT.json5",
    "content": "{\n  \"cmd\": \"ONLINE_RANK_COUNT\",\n  \"data\": {\n    \"count\": 530\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/ONLINE_RANK_TOP3.json5",
    "content": "{\n  \"cmd\": \"ONLINE_RANK_TOP3\",\n  \"data\": {\n    \"dmscore\": 112,\n    \"list\": [\n      {\n        \"msg\": \"恭喜 <%玉紫%> 成为高能用户\",\n        \"rank\": 2\n      }\n    ]\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/ONLINE_RANK_V2.json5",
    "content": "{\n  \"cmd\": \"ONLINE_RANK_V2\",\n  \"data\": {\n    \"list\": [\n      {\n        \"uid\": 1306187864,\n        \"face\": \"https://i2.hdslb.com/bfs/face/e63f7ad010d8f3108bd737fd55c23e104217248a.jpg\",\n        \"score\": \"1596\",\n        \"uname\": \"台北蘑菇_宇\",\n        \"rank\": 1,\n        \"guard_level\": 3\n      },\n      {\n        \"uid\": 388212809,\n        \"face\": \"https://i2.hdslb.com/bfs/face/1c33c76b3d3eaef35d7c61e5af5f9bf6086278b1.jpg\",\n        \"score\": \"1380\",\n        \"uname\": \"香港的蘑菇\",\n        \"rank\": 2,\n        \"guard_level\": 3\n      },\n      {\n        \"uid\": 487390101,\n        \"face\": \"https://i0.hdslb.com/bfs/face/7cb9281552047a00a8160b48cda0914d69549477.jpg\",\n        \"score\": \"1029\",\n        \"uname\": \"冯提莫华北蘑菇屯\",\n        \"rank\": 3,\n        \"guard_level\": 3\n      },\n      {\n        \"uid\": 1584980458,\n        \"face\": \"https://i1.hdslb.com/bfs/face/93a1224413253d1889bfd53c44784547f65de83c.jpg\",\n        \"score\": \"773\",\n        \"uname\": \"ACE來了來了\",\n        \"rank\": 4,\n        \"guard_level\": 3\n      },\n      {\n        \"uid\": 1183714410,\n        \"face\": \"https://i0.hdslb.com/bfs/face/7260b8063591b0653e4b0022bdcb7bd31adb2444.jpg\",\n        \"score\": \"713\",\n        \"uname\": \"者我强是\",\n        \"rank\": 5,\n        \"guard_level\": 2\n      },\n      {\n        \"uid\": 489935300,\n        \"face\": \"https://i2.hdslb.com/bfs/face/0ad62efb289cce201410d80d1f06a9f35ebddc31.jpg\",\n        \"score\": \"675\",\n        \"uname\": \"改名就中奖1219frankzn\",\n        \"rank\": 6,\n        \"guard_level\": 3\n      },\n      {\n        \"uid\": 487621860,\n        \"face\": \"http://i0.hdslb.com/bfs/face/member/noface.jpg\",\n        \"score\": \"670\",\n        \"uname\": \"1219上海大飞哥\",\n        \"rank\": 7,\n        \"guard_level\": 3\n      }\n    ],\n    \"rank_type\": \"gold-rank\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/PREPARING.json5",
    "content": "{\n  \"cmd\": \"PREPARING\",\n  \"round\": 1,\n  \"roomid\": \"47867\"\n}"
  },
  {
    "path": "bili-api/example-response/live-event/ROOM_REAL_TIME_MESSAGE_UPDATE.json",
    "content": "{\n  \"cmd\": \"ROOM_REAL_TIME_MESSAGE_UPDATE\",\n  \"data\": {\n    \"roomid\": 545068,\n    \"fans\": 2567341,\n    \"red_notice\": -1,\n    \"fans_club\": 47278\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/SEND_GIFT.json",
    "content": "{\n  \"cmd\": \"SEND_GIFT\",\n  \"data\": {\n    \"action\": \"投喂\",\n    \"batch_combo_id\": \"\",\n    \"batch_combo_send\": null,\n    \"beatId\": \"0\",\n    \"biz_source\": \"Live\",\n    \"blind_gift\": null,\n    \"broadcast_id\": 0,\n    \"coin_type\": \"silver\",\n    \"combo_resources_id\": 1,\n    \"combo_send\": null,\n    \"combo_stay_time\": 3,\n    \"combo_total_coin\": 0,\n    \"crit_prob\": 0,\n    \"demarcation\": 1,\n    \"discount_price\": 0,\n    \"dmscore\": 36,\n    \"draw\": 0,\n    \"effect\": 0,\n    \"effect_block\": 1,\n    \"face\": \"https://i1.hdslb.com/bfs/face/3ce3fb2429b61a6dc8017840c5885b2f051616f5.jpg\",\n    \"face_effect_id\": 0,\n    \"face_effect_type\": 0,\n    \"float_sc_resource_id\": 0,\n    \"giftId\": 31531,\n    \"giftName\": \"PK票\",\n    \"giftType\": 5,\n    \"gold\": 0,\n    \"guard_level\": 0,\n    \"is_first\": true,\n    \"is_naming\": false,\n    \"is_special_batch\": 0,\n    \"magnification\": 1,\n    \"medal_info\": {\n      \"anchor_roomid\": 0,\n      \"anchor_uname\": \"\",\n      \"guard_level\": 0,\n      \"icon_id\": 0,\n      \"is_lighted\": 1,\n      \"medal_color\": 13081892,\n      \"medal_color_border\": 13081892,\n      \"medal_color_end\": 13081892,\n      \"medal_color_start\": 13081892,\n      \"medal_level\": 18,\n      \"medal_name\": \"德云色\",\n      \"special\": \"\",\n      \"target_id\": 8739477\n    },\n    \"name_color\": \"\",\n    \"num\": 1,\n    \"original_gift_name\": \"\",\n    \"price\": 0,\n    \"rcost\": 195044150,\n    \"remain\": 0,\n    \"rnd\": \"1669400300110200001\",\n    \"send_master\": null,\n    \"silver\": 0,\n    \"super\": 0,\n    \"super_batch_gift_num\": 0,\n    \"super_gift_num\": 0,\n    \"svga_block\": 0,\n    \"switch\": true,\n    \"tag_image\": \"\",\n    \"tid\": \"1669400300110200001\",\n    \"timestamp\": 1669400300,\n    \"top_list\": null,\n    \"total_coin\": 0,\n    \"uid\": 396677032,\n    \"uname\": \"请看我的手势\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/STOP_LIVE_ROOM_LIST.json5",
    "content": "{\n  \"cmd\": \"STOP_LIVE_ROOM_LIST\",\n  \"data\": {\n    \"room_id_list\": [\n      25234649,\n      4132568,\n      3512524,\n      11553,\n      186390,\n      26019554,\n      26409038,\n      26437196,\n      496410,\n      23664293,\n      24018668,\n      24435506,\n      3653682,\n      820635,\n      12844500,\n      26315109,\n      26508465,\n      9861086,\n      23326858,\n      24418,\n      24603846,\n      25681642,\n      25687765,\n      26142188,\n      342530,\n      21444308,\n      23128448,\n      23513218,\n      23670828,\n      25486341,\n      25899508,\n      25899728,\n      4000898,\n      4706818,\n      5289518,\n      5410386,\n      108938,\n      192514,\n      25366186,\n      25819126,\n      26074006,\n      26188146,\n      26314983,\n      26508485,\n      406536,\n      698159,\n      7076893,\n      11717546,\n      14327116,\n      14626370,\n      147220,\n      21643350,\n      2303390,\n      26348275,\n      538440,\n      14009080,\n      23724886,\n      25488820,\n      26070750,\n      4604384,\n      5365754,\n      8588940,\n      2059262,\n      22104325,\n      25739774,\n      25880724,\n      26318794,\n      5408045,\n      24366245,\n      24682275,\n      25003965,\n      26205655,\n      6943925,\n      14201557,\n      2053869,\n      24408727,\n      25675477,\n      26393557,\n      26462447,\n      5392287,\n      22325796,\n      23914996,\n      3412797,\n      23109143,\n      23686901,\n      25228924,\n      25430191,\n      26070601,\n      12016461,\n      12122431,\n      26448781,\n      9462351,\n      13238155,\n      21565087,\n      24378798,\n      24605638,\n      25065893,\n      25085673,\n      46113,\n      6453083,\n      11092055,\n      14904215,\n      24507653,\n      25343213,\n      25791048,\n      25927972,\n      4766474,\n      5256873,\n      9035643,\n      23319049,\n      23884290,\n      24056819,\n      25118469,\n      25694943,\n      26069139,\n      7111885,\n      821819,\n      23688869,\n      23903209,\n      24684949,\n      25283390,\n      25820389,\n      4849071,\n      7587769,\n      759969,\n      9074879,\n      25383592,\n      9226449,\n      23102605,\n      24326762,\n      268302,\n      6590962,\n      21344474,\n      24082829,\n      24351518,\n      24953254,\n      25513142,\n      26129578,\n      7317916,\n      7804949,\n      8907007,\n      23285947,\n      24791078,\n      25105073,\n      21890824,\n      22569396,\n      25180663,\n      4102740,\n      8631878,\n      23232517,\n      25063568,\n      14678054,\n      25794499\n    ]\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/SUPER_CHAT_ENTRANCE.json5",
    "content": "{\n  \"cmd\": \"SUPER_CHAT_ENTRANCE\",\n  \"data\": {\n    \"icon\": \"https://i0.hdslb.com/bfs/live/0a9ebd72c76e9cbede9547386dd453475d4af6fe.png\",\n    \"jump_url\": \"https://live.bilibili.com/p/html/live-app-superchat2/index.html?is_live_half_webview=1&hybrid_half_ui=1,3,100p,70p,ffffff,0,30,100;2,2,375,100p,ffffff,0,30,100;3,3,100p,70p,ffffff,0,30,100;4,2,375,100p,ffffff,0,30,100;5,3,100p,60p,ffffff,0,30,100;6,3,100p,60p,ffffff,0,30,100;7,3,100p,60p,ffffff,0,30,100\",\n    \"status\": 0\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/SUPER_CHAT_MESSAGE.json5",
    "content": "{\n  \"cmd\": \"SUPER_CHAT_MESSAGE\",\n  \"data\": {\n    \"background_bottom_color\": \"#2A60B2\",\n    \"background_color\": \"#EDF5FF\",\n    \"background_color_end\": \"#405D85\",\n    \"background_color_start\": \"#3171D2\",\n    \"background_icon\": \"\",\n    \"background_image\": \"https://i0.hdslb.com/bfs/live/a712efa5c6ebc67bafbe8352d3e74b820a00c13e.png\",\n    \"background_price_color\": \"#7497CD\",\n    \"color_point\": 0.7,\n    \"dmscore\": 24,\n    \"end_time\": 1669434178,\n    \"gift\": {\n      \"gift_id\": 12000,\n      \"gift_name\": \"醒目留言\",\n      \"num\": 1\n    },\n    \"id\": 5639015,\n    \"is_ranked\": 1,\n    \"is_send_audit\": 0,\n    \"medal_info\": {\n      \"anchor_roomid\": 21987615,\n      \"anchor_uname\": \"原神\",\n      \"guard_level\": 0,\n      \"icon_id\": 0,\n      \"is_lighted\": 0,\n      \"medal_color\": \"#5d7b9e\",\n      \"medal_color_border\": 12632256,\n      \"medal_color_end\": 12632256,\n      \"medal_color_start\": 12632256,\n      \"medal_level\": 8,\n      \"medal_name\": \"粉丝团\",\n      \"special\": \"\",\n      \"target_id\": 401742377\n    },\n    \"message\": \"多喝热水(・∀・)\",\n    \"message_font_color\": \"#A3F6FF\",\n    \"message_trans\": \"\",\n    \"price\": 30,\n    \"rate\": 1000,\n    \"start_time\": 1669434118,\n    \"time\": 60,\n    \"token\": \"72481621\",\n    \"trans_mark\": 0,\n    \"ts\": 1669434118,\n    \"uid\": 11437137,\n    \"user_info\": {\n      \"face\": \"https://i0.hdslb.com/bfs/face/3d2b2e6afbf9aed3eccf7d00a7c02d2351b22cf3.jpg\",\n      \"face_frame\": \"\",\n      \"guard_level\": 0,\n      \"is_main_vip\": 0,\n      \"is_svip\": 0,\n      \"is_vip\": 0,\n      \"level_color\": \"#969696\",\n      \"manager\": 0,\n      \"name_color\": \"#666666\",\n      \"title\": \"0\",\n      \"uname\": \"Phain-\",\n      \"user_level\": 6\n    }\n  },\n  \"roomid\": 1017\n}"
  },
  {
    "path": "bili-api/example-response/live-event/SUPER_CHAT_MESSAGE_JPN.json5",
    "content": "{\n  \"cmd\": \"SUPER_CHAT_MESSAGE_JPN\",\n  \"data\": {\n    \"id\": \"5639015\",\n    \"uid\": \"11437137\",\n    \"price\": 30,\n    \"rate\": 1000,\n    \"message\": \"多喝热水(・∀・)\",\n    \"message_jpn\": \"\",\n    \"is_ranked\": 1,\n    \"background_image\": \"https://i0.hdslb.com/bfs/live/a712efa5c6ebc67bafbe8352d3e74b820a00c13e.png\",\n    \"background_color\": \"#EDF5FF\",\n    \"background_icon\": \"\",\n    \"background_price_color\": \"#7497CD\",\n    \"background_bottom_color\": \"#2A60B2\",\n    \"ts\": 1669434119,\n    \"token\": \"9555B0B6\",\n    \"medal_info\": {\n      \"icon_id\": 0,\n      \"target_id\": 401742377,\n      \"special\": \"\",\n      \"anchor_uname\": \"原神\",\n      \"anchor_roomid\": 21987615,\n      \"medal_level\": 8,\n      \"medal_name\": \"粉丝团\",\n      \"medal_color\": \"#5d7b9e\"\n    },\n    \"user_info\": {\n      \"uname\": \"Phain-\",\n      \"face\": \"https://i0.hdslb.com/bfs/face/3d2b2e6afbf9aed3eccf7d00a7c02d2351b22cf3.jpg\",\n      \"face_frame\": \"\",\n      \"guard_level\": 0,\n      \"user_level\": 6,\n      \"level_color\": \"#969696\",\n      \"is_vip\": 0,\n      \"is_svip\": 0,\n      \"is_main_vip\": 0,\n      \"title\": \"0\",\n      \"manager\": 0\n    },\n    \"time\": 59,\n    \"start_time\": 1669434118,\n    \"end_time\": 1669434178,\n    \"gift\": {\n      \"num\": 1,\n      \"gift_id\": 12000,\n      \"gift_name\": \"醒目留言\"\n    }\n  },\n  \"roomid\": \"1017\"\n}"
  },
  {
    "path": "bili-api/example-response/live-event/USER_TOAST_MSG.json5",
    "content": "{\n  \"cmd\": \"USER_TOAST_MSG\",\n  \"data\": {\n    \"anchor_show\": true,\n    \"color\": \"#00D1F1\",\n    \"dmscore\": 90,\n    \"effect_id\": 397,\n    \"end_time\": 1669435051,\n    \"face_effect_id\": 44,\n    \"gift_id\": 10003,\n    \"guard_level\": 3,\n    \"is_show\": 0,\n    \"num\": 1,\n    \"op_type\": 3,\n    \"payflow_id\": \"2211261157000842110278874\",\n    \"price\": 138000,\n    \"role_name\": \"舰长\",\n    \"room_effect_id\": 590,\n    \"start_time\": 1669435051,\n    \"svga_block\": 0,\n    \"target_guard_count\": 972,\n    \"toast_msg\": \"<%玉紫%> 续费了舰长，今天是TA陪伴主播的第691天\",\n    \"uid\": 6501027,\n    \"unit\": \"月\",\n    \"user_show\": true,\n    \"username\": \"玉紫\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/WATCHED_CHANGE.json5",
    "content": "{\n  \"cmd\": \"WATCHED_CHANGE\",\n  \"data\": {\n    \"num\": 305504,\n    \"text_small\": \"30.5万\",\n    \"text_large\": \"30.5万人看过\"\n  }\n}"
  },
  {
    "path": "bili-api/example-response/live-event/WIDGET_BANNER.json5",
    "content": "{\n  \"cmd\": \"WIDGET_BANNER\",\n  \"data\": {\n    \"timestamp\": 1669400283,\n    \"widget_list\": {\n      \"280\": {\n        \"id\": 280,\n        \"title\": \"BLS年终赛·地区争锋赛\",\n        \"cover\": \"\",\n        \"web_cover\": \"\",\n        \"tip_text\": \"地区争锋赛\",\n        \"tip_text_color\": \"#FFFFFF\",\n        \"tip_bottom_color\": \"#3F2D25\",\n        \"jump_url\": \"https://live.bilibili.com/activity/live-activity-battle/index.html?app_name=bls_winter_2022&is_live_half_webview=1&hybrid_rotate_d=1&hybrid_half_ui=1,3,100p,70p,0,0,0,0,12,0;2,2,375,100p,0,0,0,0,12,0;3,3,100p,70p,0,0,0,0,12,0;4,2,375,100p,0,0,0,0,12,0;5,3,100p,70p,0,0,0,0,12,0;6,3,100p,70p,0,0,0,0,12,0;7,3,100p,70p,0,0,0,0,12,0;8,3,100p,70p,0,0,0,0,12,0&room_id=545068&uid=8739477#/area\",\n        \"url\": \"\",\n        \"stay_time\": 5,\n        \"site\": 1,\n        \"platform_in\": [\n          \"live\",\n          \"blink\",\n          \"live_link\",\n          \"web\",\n          \"pc_link\"\n        ],\n        \"type\": 1,\n        \"band_id\": 101318,\n        \"sub_key\": \"\",\n        \"sub_data\": \"%7B%22act_status%22%3A1%2C%22stage%22%3A1%2C%22text%22%3A%22%22%2C%22stage_1%22%3A%7B%22current_value%22%3A4923%2C%22target_value%22%3A50000%2C%22current_level%22%3A5%2C%22is_all_complete%22%3A2%7D%2C%22stage_2%22%3A%7B%22settlement%22%3A0%2C%22rank%22%3A0%2C%22score%22%3A0%2C%22rank_type%22%3A0%2C%22rank_distance_score%22%3A0%7D%2C%22stage_3%22%3A%7B%22settlement%22%3A0%2C%22show_type%22%3A0%2C%22rank_all%22%3A0%2C%22rank%22%3A0%2C%22score%22%3A0%2C%22rank_type%22%3A0%2C%22rank_distance_score%22%3A0%7D%2C%22stage_4%22%3A%7B%22settlement%22%3A0%2C%22rank%22%3A0%2C%22score%22%3A0%2C%22rank_type%22%3A0%2C%22rank_distance_score%22%3A0%2C%22team_name%22%3A%22%22%2C%22anchor_img%22%3A%22%22%7D%2C%22notice%22%3A%7B%22type%22%3A0%2C%22sub_type%22%3A0%2C%22number%22%3A%22%22%2C%22is_get%22%3A0%2C%22rank%22%3A0%2C%22pond_pk_amount%22%3A0%2C%22rank_distance_score%22%3A0%2C%22red_packet_multiple%22%3A0%2C%22red_packet_amount%22%3A0%2C%22time%22%3A0%2C%22timeout%22%3A0%2C%22level%22%3A0%7D%7D\",\n        \"is_add\": true\n      }\n    }\n  }\n}"
  },
  {
    "path": "bili-api/grpc/.gitignore",
    "content": "/build"
  },
  {
    "path": "bili-api/grpc/build.gradle.kts",
    "content": "import com.google.protobuf.gradle.proto\n\nplugins {\n    //id(\"java-library\")\n    alias(gradleLibs.plugins.google.protobuf)\n    alias(gradleLibs.plugins.kotlin.jvm)\n}\n\ndependencies {\n    api(libs.grpc.kotlin.stub)\n    api(libs.grpc.okhttp)\n    api(libs.grpc.protobuf)\n    api(libs.grpc.stub)\n    api(libs.protobuf.kotlin)\n    implementation(libs.kotlinx.coroutines)\n}\n\nsourceSets[\"main\"].proto {\n    srcDir(\"./proto\")\n    ProtobufConfiguration.excludeProtoFiles.forEach(::exclude)\n}\n\nprotobuf {\n    protoc {\n        artifact = \"com.google.protobuf:protoc:${libs.versions.protobuf.get()}\"\n    }\n    plugins {\n        create(\"java\") {\n            artifact = \"io.grpc:protoc-gen-grpc-java:${libs.versions.grpc.asProvider().get()}\"\n        }\n        create(\"grpc\") {\n            artifact = \"io.grpc:protoc-gen-grpc-java:${libs.versions.grpc.asProvider().get()}\"\n        }\n        create(\"grpckt\") {\n            artifact = \"io.grpc:protoc-gen-grpc-kotlin:${libs.versions.grpc.kotlin.get()}:jdk8@jar\"\n        }\n    }\n    generateProtoTasks {\n        all().forEach {\n            it.builtins {\n                named(\"java\") {\n                    //option(\"lite\")\n                }\n                create(\"kotlin\") {\n                    //option(\"lite\")\n                }\n            }\n            it.plugins {\n                create(\"grpc\") {\n                    //option(\"lite\")\n                }\n                create(\"grpckt\") {\n                    //option(\"lite\")\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/account/fission/v1/fission.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.account.fission.v1;\n\noption java_multiple_files = true;\n\n// Fission裂变\nservice Fission {\n  // 活动入口\n  rpc Entrance (EntranceReq) returns (EntranceReply);\n  // 首页弹窗\n  rpc Window (WindowReq) returns (WindowReply);\n  //\n  rpc Privacy (PrivacyReq) returns (PrivacyReply);\n}\n\n// 动画效果\nmessage AnimateIcon {\n  // icon文件\n  string icon = 1;\n  // 动效json文件\n  string json = 2;\n}\n\n// 活动入口-响应\nmessage EntranceReply {\n  // 展示图标\n  string icon = 1;\n  // 活动名称\n  string name = 2;\n  // 活动跳转链接\n  string url = 3;\n  // 动画效果\n  AnimateIcon animate_icon = 4;\n}\n\n// 活动入口-请求\nmessage EntranceReq {}\n\n//\nmessage PrivacyReply {\n  //\n  string message = 1;\n}\n\n//\nmessage PrivacyReq {\n  //\n  string activity_uid = 1;\n}\n\n//首页弹窗-响应\nmessage WindowReply {\n  // 弹窗类型\n  // 0:弹窗 1:普通页面\n  int32 type = 1;\n  // 跳转链接\n  string url = 2;\n  // 上报数据字段\n  string report_data = 3;\n}\n\n// 首页弹窗-请求\nmessage WindowReq {\n\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/ad/v1/ad.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.ad.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/any.proto\";\nimport \"google/protobuf/wrappers.proto\";\n\n// 自动播放视频\nmessage AdAutoPlayVideoDto {\n  // avid\n  int64 avid = 1;\n  // cid\n  int64 cid = 2;\n  // 分P\n  int64 page = 3;\n  //\n  string from = 4;\n  // 是否自动播放\n  string url = 5;\n  // 是否自动播放\n  string cover = 6;\n  // 是否自动播放\n  bool auto_play = 7;\n  // 按钮是否动态变色\n  bool btn_dyc_color = 8;\n  // 按钮动态变色时间 ms\n  int32 btn_dyc_time = 9;\n  // 用于做联播是否是同一个视频的id\n  int64 biz_id = 10;\n  // 开始播放三方监控\n  repeated string process0_urls = 11;\n  // 播放3S三方监控\n  repeated string play_3s_urls = 12;\n  // 播放5S三方监控\n  repeated string play_5s_urls = 13;\n  // 横竖屏\n  int32 orientation = 14;\n}\n\n// 商业标信息\nmessage AdBusinessMarkDto {\n  // 商业标样式\n  // 0:不展示标 1:实心+文字 2:空心框+文字 3:纯文字标 4:纯图片标\n  int32 type = 1;\n  // 商业标文案\n  string text = 2;\n  // 商业标文案颜色,如#80FFFFFF RGBA\n  string text_color = 3;\n  // 夜间模式文字色\n  string text_color_night = 4;\n  // 背景色\n  string bg_color = 5;\n  // 夜间模式背景色\n  string bg_color_night = 6;\n  // 边框色\n  string border_color = 7;\n  // 夜间模式边框色\n  string border_color_night = 8;\n  // 图片商业标\n  string img_url = 9;\n  // 图片高度\n  int32 img_height = 10;\n  // 图片宽度\n  int32 img_width = 11;\n  //\n  string bg_border_color = 12;\n}\n\n// 按钮\nmessage AdButtonDto {\n  // 类型\n  // 1:落地页 2:应用唤起 3:应用下载\n  int32 type = 1;\n  // 按钮文案\n  string text = 2;\n  // 按钮跳转地址\n  string jump_url = 3;\n  // 跳转监测链接\n  string report_urls = 4;\n  // 唤起schema\n  string dlsuc_callup_url = 5;\n  // 游戏id\n  int32 game_id = 6;\n  // 游戏监控字段\n  string game_monitor_param = 7;\n  //\n  int32 game_channel_id = 8;\n  //\n  string game_channel_extra = 9;\n}\n\n// 卡片\nmessage AdCardDto {\n  // 卡片类型\n  int32 card_type = 1;\n  // 标题\n  string title = 2;\n  // 描述\n  string desc = 3;\n  // 额外描述\n  string extra_desc = 4;\n  // 长描述\n  string long_desc = 5;\n  // 短标题, 弹幕广告目录面板标题\n  string short_title = 6;\n  // 弹幕/浮层广告的弹幕标题\n  string danmu_title = 7;\n  // 弹幕/浮层广告的弹幕高度，整型，分母为100\n  int32 danmu_height = 8;\n  // 弹幕/浮层广告的弹幕宽度，整型，分母为100\n  int32 danmu_width = 9;\n  // 弹幕/浮层广告生存时间，单位为毫秒\n  int32 danmu_life = 10;\n  // 弹幕/浮层开始时间，单位为毫秒\n  int32 danmu_begin = 11;\n  // 背景色值（含透明度）如#80FFFFFF\n  string danmu_color = 12;\n  // 弹幕/浮层广告H5落地页\n  string danmu_h5url = 13;\n  // 弹幕/浮层 广告icon\n  string danmu_icon = 14;\n  // 折叠时间，永驻浮层折叠时间，单位为毫秒\n  int32 fold_time = 15;\n  // 广告标文案\n  string ad_tag = 16;\n  // cover数组\n  repeated AdCoverDto covers = 17;\n  // 卡片跳转链接\n  string jump_url = 18;\n  //\n  string imax_landing_page_json_string = 19;\n  // app唤起schema\n  string callup_url = 20;\n  // univeral link域名\n  string universal_app = 21;\n  // 原价, 单位为分\n  string ori_price = 22;\n  // 现价, 同上\n  int32 cur_price = 23;\n  // 券后/现价 价格描述\n  string price_desc = 24;\n  // 价格单位符号\n  string price_symbol = 25;\n  // 券后价格 \"1000\"\n  string goods_cur_price = 26;\n  // 原价 \"¥1002\"\n  string goods_ori_price = 27;\n  // 开放平台商品\n  AdGoodDto good = 28;\n  // 打分? 满分为100\n  int32 rank = 29;\n  // 热度\n  int32 hot_score = 30;\n  // 按钮\n  AdButtonDto button = 31;\n  // 广告主logo\n  string adver_logo = 32;\n  // 广告主name\n  string adver_name = 33;\n  // 广告主主页链接\n  string adver_page_url = 34;\n  // 视频弹幕，视频广告用\n  repeated string video_barrage = 35;\n  // 商业标信息\n  AdBusinessMarkDto ad_tag_style = 36;\n  // 自动播放视频\n  AdAutoPlayVideoDto video = 37;\n  // 反馈面板功能模块，屏蔽、投诉、广告介绍\n  AdFeedbackPanelDto feedback_panel = 38;\n  //\n  int64 adver_mid = 39;\n  //\n  int64 adver_account_id = 40;\n  //\n  string duration = 41;\n  //\n  repeated QualityInfo quality_infos = 42;\n  // 动态广告文本\n  string dynamic_text = 43;\n  // 广告主信息\n  AdverDto adver = 44;\n  // 评分\n  int32 grade_level = 45;\n  //\n  bool support_transition = 46;\n  //\n  string transition = 47;\n  //\n  int32 under_player_interaction_style = 48;\n  //\n  string imax_landing_page_v2 = 49;\n  //\n  SubCardModule subcard_module = 50;\n  //\n  int32 grade_denominator = 51;\n  //\n  int32 star_level = 52;\n  //\n  Bulletin bulletin = 53;\n  //\n  Gift gift = 54;\n  //\n  repeated string game_tags = 55;\n  //\n  int32 ori_mark_hidden = 56;\n  //\n  bool use_multi_cover = 57;\n  //\n  WxProgramInfo wx_program_info = 58;\n  //\n  AndroidGamePageRes android_game_page_res = 59;\n  //\n  NotClickableArea not_clickable_area = 60;\n  //\n  ForwardReply forward_reply = 61;\n}\n\n// 额外广告数据\nmessage AdContentExtraDto {\n  // 动态布局\n  string layout = 1;\n  // 展现监控url\n  repeated string show_urls = 2;\n  // 点击监控url\n  repeated string click_urls = 3;\n  // 弹幕创意列表展示第三方上报\n  repeated string danmu_list_show_urls = 4;\n  // 弹幕创意列表点击第三方上报\n  repeated string danmu_list_click_urls = 5;\n  // 弹幕详情页展示第三方上报\n  repeated string danmu_detail_show_urls = 6;\n  // 弹幕商品添加购物车第三方上报\n  repeated string danmu_trolley_add_urls = 7;\n  // useWebView默认false\n  bool use_ad_web_v2 = 8;\n  // app唤起白名单\n  repeated string open_whitelist = 9;\n  // app下载白名单\n  AppPackageDto download_whitelist = 10;\n  // 卡片相关信息\n  AdCardDto card = 11;\n  // 视频播放和弹幕播放上报控制时间 ms\n  int32 report_time = 12;\n  // 是否优先唤起app store\n  int32 appstore_priority = 13;\n  // 广告售卖类型\n  int32 sales_type = 14;\n  // 落地页是否预加载\n  int32 preload_landingpage = 15;\n  // 是否需要展示风险行业提示\n  bool special_industry = 16;\n  // 风险行业提示\n  string special_industry_tips = 17;\n  // 是否展示下载弹框\n  bool enable_download_dialog = 18;\n  // 是否允许分享\n  bool enable_share = 19;\n  // 个人空间广告入口类型\n  // 1:橱窗 2:商品店铺 3:小程序\n  int32 upzone_entrance_type = 20;\n  // 个人空间广告入口上报id,橱窗id(当前用Mid)、店铺id或者小程序id\n  int32 upzone_entrance_report_id = 21;\n  // 分享数据\n  AdShareInfoDto share_info = 22;\n  // topview图片链接，闪屏预下载用\n  string topview_pic_url = 23;\n  // topview视频链接，闪屏预下载用\n  string topview_video_url = 24;\n  // 点击区域\n  // 0:表示banner可点击 1:表示素材可点击\n  int32 click_area = 25;\n  // 店铺\n  int64 shop_id = 26;\n  // up主\n  int64 up_mid = 27;\n  // 回传id\n  string track_id = 28;\n  // 商店直投\n  int32 enable_store_direct_launch = 29;\n  // DPA2.0商品ID\n  int64 product_id = 30;\n  //\n  bool enable_double_jump = 31;\n  //\n  repeated string show1s_urls = 32;\n  //\n  string from_track_id = 33;\n  //\n  bool store_callup_card = 34;\n  //\n  int32 landingpage_download_style = 35;\n  //\n  int32 special_industry_style = 36;\n  //\n  bool enable_h5_alert = 37;\n  //\n  int32 macro_replace_priority = 38;\n  //\n  int32 feedback_panel_style = 39;\n  //\n  string appstore_url = 40;\n  //\n  int32 enable_h5_pre_load = 41;\n  //\n  string h5_pre_load_url = 42;\n  //\n  string cm_from_track_id = 43;\n}\n\n// 广告卡片封面数据\nmessage AdCoverDto {\n  // 图片链接\n  string url = 1;\n  // 动图循环次数\n  // 0:无限循环\n  int32 loop = 2;\n  // 图片点击跳转地址，截至目前为空\n  string jump_url = 3;\n  // 跳转监测链接， 数组，单个图片的监控，出区别于click_urls，应前端要求。（此字段截至目前为空，使用时需再次确认）\n  repeated string report_urls = 4;\n  // 图片高度\n  int32 image_height = 5;\n  // 图片宽度\n  int32 image_width = 6;\n}\n\n// 广告内容\nmessage AdDto {\n  // 广告创意ID\n  int64 creative_id = 1;\n  // 广告闭环上报回传数据\n  string ad_cb = 2;\n  // 额外广告数据\n  AdContentExtraDto extra = 3;\n  // 广告标记\n  int32 cm_mark = 4;\n  //\n  int64 top_view_id = 5;\n  //\n  int32 creative_type = 6;\n  //\n  int32 card_type = 7;\n  //\n  int32 creative_style = 8;\n  //\n  int32 is_ad = 9;\n  //\n  CreativeDto creative_content = 10;\n}\n\n// 反馈面板功能模块\nmessage AdFeedbackPanelDto {\n  // 面板类型，广告、推广\n  string panel_type_text = 1;\n  // 反馈面版信息\n  repeated AdFeedbackPanelModuleDto feedback_panel_detail = 2;\n  //\n  string toast = 3;\n  //\n  string open_rec_tips = 4;\n  //\n  string close_rec_tips = 5;\n}\n\n// 反馈面版信息\nmessage AdFeedbackPanelModuleDto {\n  // 模块id\n  int32 module_id = 1;\n  // icon url\n  string icon_url = 2;\n  // 跳转类型\n  // 1:气泡 2:H5\n  int32 jump_type = 3;\n  // 跳转地址\n  string jump_url = 4;\n  // 文案\n  string text = 5;\n  // 二级文案数组\n  repeated AdSecondFeedbackPanelDto secondary_panel = 6;\n  //\n  string sub_text = 7;\n}\n\n// 开放平台商品\nmessage AdGoodDto {\n  // 电商商品ID\n  int64 item_id = 1;\n  // 电商SKU ID\n  int64 sku_id = 2;\n  // 店铺ID\n  int64 shop_id = 3;\n  // SKU库存\n  int64 sku_num = 4;\n}\n\n// 有弹幕的ogv ep\nmessage AdOgvEpDto {\n  // 分集epid\n  int64 epid = 1;\n  // 是否显示 \"荐\"\n  bool has_recommend = 2;\n}\n\n// 广告控制\nmessage AdsControlDto {\n  // 视频是否有弹幕，如有，需请求弹幕广告\n  int32 has_danmu = 1;\n  // 有弹幕的分P视频的cid，已弃用\n  repeated int64 cids = 2;\n  // 有弹幕的ogv ep\n  repeated AdOgvEpDto eps = 3;\n}\n\n// 二级文案\nmessage AdSecondFeedbackPanelDto {\n  // 屏蔽理由id\n  int32 reason_id = 1;\n  // 理由文案\n  string text = 2;\n}\n\n// 分享\nmessage AdShareInfoDto {\n  // 分享标题\n  string title = 1;\n  // 分享副标题\n  string subtitle = 2;\n  // 分享图片url\n  string image_url = 3;\n}\n\n// 广告主信息\nmessage AdverDto {\n  //\n  int64 adver_id = 1;\n  //\n  string adver_logo = 2;\n  //\n  string adver_name = 3;\n  //\n  int32 adver_type = 4;\n  //\n  string adver_page_url = 5;\n  //\n  string adver_desc = 6;\n}\n\n//\nmessage AndroidGamePageRes {\n  //\n  Module1 module1 = 1;\n  //\n  Module3 module3 = 2;\n  //\n  Module4 module4 = 3;\n  //\n  Module5 module5 = 4;\n  //\n  Module6 module6 = 5;\n  //\n  Module7 module7 = 6;\n  //\n  Module8 module8 = 7;\n  //\n  Module9 module9 = 8;\n  //\n  Module10 module10 = 9;\n  //\n  Module11 module11 = 10;\n  //\n  Module12 module12 = 11;\n  //\n  Module13 module13 = 12;\n  //\n  repeated int32 module_seq = 13;\n  //\n  string background_color = 14;\n  //\n  Module14 module14 = 15;\n}\n\n//\nmessage AndroidTag {\n  //\n  string text = 1;\n  //\n  int32 type = 2;\n}\n\n// app下载白名单\nmessage AppPackageDto {\n  // 包大小(单位bytes)\n  int64 size = 1;\n  //\n  string display_name = 2;\n  //\n  string apk_name = 3;\n  // url\n  string url = 4;\n  // bili schema url\n  string bili_url = 5;\n  // 包md5\n  string md5 = 6;\n  // 包icon\n  string icon = 7;\n  // 开发者姓名\n  string dev_name = 8;\n  // 权限地址\n  string auth_url = 9;\n  // 权限名，逗号隔开\n  string auth_name = 10;\n  // 版本\n  string version = 11;\n  // 更新时间,yy-mm-hh格式\n  string update_time = 12;\n  // 隐私协议标题\n  string privacy_name = 13;\n  // 隐私协议url\n  string privacy_url = 14;\n}\n\n//\nmessage Bulletin {\n  //\n  string tag_text = 1;\n  //\n  string text = 2;\n}\n\n//\nmessage Comment {\n  //\n  int64 game_base_id = 1;\n  //\n  string user_name = 2;\n  //\n  string user_face = 3;\n  //\n  int32 user_level = 4;\n  //\n  string comment_no = 5;\n  //\n  int32 grade = 6;\n  //\n  string content = 7;\n  //\n  int32 up_count = 8;\n}\n\n//\nmessage CreativeDto {\n  //\n  string title = 1;\n  //\n  string description = 2;\n  //\n  string image_url = 3;\n  //\n  string image_md5 = 4;\n  //\n  string url = 5;\n  //\n  string click_url = 6;\n  //\n  string show_url = 7;\n  //\n  int64 video_id = 8;\n  //\n  string thumbnail_url = 9;\n  //\n  string thumbnail_url_md5 = 10;\n  //\n  string logo_url = 11;\n  //\n  string logo_md5 = 12;\n  //\n  string username = 13;\n}\n\n//\nmessage CustomPlayUrl {\n  //\n  int32 play_time = 1;\n  //\n  repeated string urls = 2;\n}\n\n//\nmessage ForwardReply {\n  //\n  int64 comment_id = 1;\n  //\n  string message = 2;\n  //\n  string highlight_text = 3;\n  //\n  string highlight_prefix_icon = 4;\n  //\n  string callup_url = 5;\n  //\n  string jump_url = 6;\n  //\n  int32 jump_type = 7;\n  //\n  string author_name = 8;\n  //\n  string author_icon = 9;\n}\n\n//\nmessage Gift {\n  //\n  string icon = 1;\n  //\n  string night_icon = 2;\n  //\n  string text = 3;\n  //\n  string url = 4;\n}\n\n//\nmessage IosGamePageRes {\n  //\n  string logo = 1;\n  //\n  string name = 2;\n  //\n  string sub_titile = 3;\n  //\n  repeated string image_url = 4;\n  //\n  string desc = 5;\n  //\n  AdButtonDto game_button = 6;\n  //\n  double grade = 7;\n  //\n  string rank_num = 8;\n  //\n  string rank_name = 9;\n}\n\n//\nmessage Module1 {\n  //\n  string game_name = 1;\n  //\n  string game_icon = 2;\n  //\n  string developer_input_name = 3;\n  //\n  repeated AndroidTag tag_list = 4;\n}\n\n//\nmessage Module3 {\n  //\n  bool display = 1;\n  //\n  repeated QualityParmas quality_params = 3;\n}\n\n//\nmessage Module4 {\n  //\n  bool display = 1;\n  //\n  int32 gift_num = 2;\n  //\n  string gift_name = 3;\n  //\n  int32 gift_icon_num = 4;\n  //\n  repeated string icon_urls = 5;\n}\n\n//\nmessage Module5 {\n  //\n  bool display = 1;\n  //\n  string game_summary = 2;\n}\n\n//\nmessage Module6 {\n  //\n  bool display = 1;\n  //\n  string game_desc = 2;\n}\n\n//\nmessage Module7 {\n  //\n  bool display = 1;\n  //\n  repeated ScreenShots screen_shots = 2;\n}\n\n//\nmessage Module8 {\n  //\n  bool display = 1;\n  //\n  repeated string tag_list = 2;\n}\n\n//\nmessage Module9 {\n  //\n  bool display = 1;\n  //\n  string dev_introduction = 2;\n}\n\n//\nmessage Module10 {\n  //\n  bool display = 1;\n  //\n  string latest_update = 2;\n}\n\n//\nmessage Module11 {\n  //\n  bool display = 1;\n  //\n  repeated int32 star_number_list = 2;\n  //\n  string comment_str = 3;\n  //\n  double grade = 4;\n}\n\n//\nmessage Module12 {\n  //\n  bool display = 1;\n  //\n  repeated Comment comment_list = 2;\n  //\n  string comment_num = 3;\n  //\n  bool show_all_comment = 4;\n}\n\n//\nmessage Module13 {\n  //\n  int64 pkg_size = 1;\n  //\n  string customer_service = 2;\n  //\n  string website = 3;\n  //\n  string authority = 4;\n  //\n  string privacy = 5;\n  //\n  string developer_name = 6;\n  //\n  string update_time = 7;\n  //\n  string game_version = 8;\n  //\n  string android_pkg_name = 9;\n}\n\n//\nmessage Module14 {\n  //\n  repeated Reward reward_list = 1;\n  //\n  bool display = 2;\n}\n\n//\nmessage NotClickableArea {\n  //\n  int32 x = 1;\n  //\n  int32 y = 2;\n  //\n  int32 z = 3;\n}\n\n//\nmessage QualityInfo {\n  //\n  string icon = 1;\n  //\n  string text = 2;\n  //\n  bool is_bg = 3;\n  //\n  string bg_color = 4;\n  //\n  string bg_color_night = 5;\n}\n\n//\nmessage QualityParmas {\n  //\n  string first_line = 1;\n  //\n  string second_line = 2;\n  //\n  double grade = 3;\n  //\n  string rank_icon = 4;\n  //\n  int32 quality_type = 5;\n}\n\n//\nmessage Reward {\n  //\n  int32 level = 1;\n  //\n  string title = 2;\n  //\n  string content = 3;\n  //\n  string pic = 4;\n  //\n  bool reach = 5;\n}\n\n//\nmessage ScreenShots {\n  //\n  string url = 1;\n  //\n  int32 height = 2;\n  //\n  int32 width = 3;\n  //\n  int32 seq = 4;\n}\n\n// 广告数据\nmessage SourceContentDto {\n  // 广告请求id\n  string request_id = 1;\n  // 广告资源位source ID\n  int32 source_id = 2;\n  // 广告资源位resource ID\n  int32 resource_id = 3;\n  // 广告位上报标记,对广告返回数据恒为true\n  bool is_ad_loc = 4;\n  // 与天马现有逻辑一致, 0有含义\n  // 0:内容 1:广告\n  google.protobuf.Int32Value server_type = 5;\n  // 客户端IP回传拼接\n  string client_ip = 6;\n  // 广告卡片位置在一刷中的位置, 天马用, 0有含义\n  google.protobuf.Int32Value card_index = 7;\n  // 广告资源位source 位次\n  int32 index = 8;\n  // 广告内容\n  AdDto ad_content = 9;\n}\n\n//\nmessage SubCardModule {\n  //\n  string subcard_type = 1;\n  //\n  string icon = 2;\n  //\n  string desc = 3;\n  //\n  string rank_stars = 4;\n  //\n  string amount_number = 5;\n  //\n  string avatar = 6;\n  //\n  string title = 7;\n  //\n  AdButtonDto button = 8;\n  //\n  repeated TagInfo tag_infos = 9;\n}\n\n//\nmessage Tab2ExtraDto {\n  //\n  string cover_url = 1;\n  //\n  string title = 2;\n  //\n  string desc = 3;\n  //\n  AdButtonDto button = 5;\n  //\n  int32 auto_animate_time_ms = 6;\n  //\n  bool enable_click = 7;\n  //\n  string panel_url = 8;\n  //\n  repeated AppPackageDto download_whitelist = 9;\n  //\n  repeated string open_whitelist = 10;\n  //\n  bool use_ad_web_v2 = 11;\n  //\n  bool enable_store_direct_launch = 12;\n  //\n  int32 sales_type = 13;\n  //\n  int32 landingpage_download_style = 15;\n  //\n  int32 appstore_priority = 16;\n  //\n  string appstore_url = 17;\n  //\n  int32 appstore_delay_time = 18;\n  //\n  int32 page_cover_type = 19;\n  //\n  int32 page_pull_type = 20;\n  //\n  AndroidGamePageRes android_game_page_res = 21;\n  //\n  IosGamePageRes ios_game_page_res = 22;\n  //\n  AdBusinessMarkDto ad_tag_style = 23;\n  //\n  AdFeedbackPanelDto feedback_panel = 24;\n  //\n  string ad_cb = 25;\n  //\n  int32 url_type = 26;\n}\n\n//\nmessage TabExtraDto {\n  //\n  string tab_url = 1;\n  //\n  int32 enable_store_direct_launch = 2;\n  //\n  int32 store_callup_card = 3;\n  //\n  int32 sales_type = 4;\n  //\n  repeated AppPackageDto download_whitelist = 5;\n  //\n  bool special_industry = 6;\n  //\n  string special_industry_tips = 7;\n  //\n  repeated string open_whitelist = 8;\n  //\n  int32 landingpage_download_style = 9;\n  //\n  int32 appstore_priority = 10;\n  //\n  bool use_ad_web_v2 = 11;\n  //\n  bool enable_download_dialog = 12;\n  //\n  string appstore_url = 13;\n  //\n  int32 appstore_delay_time = 14;\n}\n\n//\nmessage TabInfoDto {\n  //\n  string tab_name = 1;\n  //\n  google.protobuf.Any extra = 2;\n  //\n  int32 tab_version = 3;\n}\n\n//\nmessage TagInfo {\n  //\n  string text = 1;\n  //\n  string text_color = 2;\n  //\n  string text_color_night = 3;\n  //\n  string bg_color = 4;\n  //\n  string bg_color_night = 5;\n  //\n  string border_color = 6;\n  //\n  string border_color_night = 7;\n  //\n  string type = 8;\n}\n\n//\nmessage WxProgramInfo {\n  //\n  string org_id = 1;\n  //\n  string name = 2;\n  //\n  string path = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/api/player/v1/player.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.api.player.v1;\n\noption java_multiple_files = true;\n\n// 心跳上报\nservice Heartbeat {\n  // 客户端心跳上报\n  rpc Mobile(HeartbeatReq) returns (HeartbeatReply);\n}\n\n// 客户端心跳上报-响应\nmessage HeartbeatReply {\n  // 时间戳\n  int64 ts = 1;\n}\n\n// 客户端心跳上报-请求\nmessage HeartbeatReq {\n  //\n  int64 server_time = 1;\n  //\n  string session = 2;\n  // 用户 mid\n  int64 mid = 3;\n  // 稿件 avid\n  int64 aid = 4;\n  // 视频 cid\n  int64 cid = 5;\n  //\n  string sid = 6;\n  //\n  int64 epid = 7;\n  //\n  string type = 8;\n  //\n  int32 sub_type = 9;\n  //\n  int32 quality = 10;\n  //\n  int64 total_time = 11;\n  //\n  int64 paused_time = 12;\n  //\n  int64 played_time = 13;\n  //\n  int64 video_duration = 14;\n  //\n  string play_type = 15;\n  //\n  int32 network_type = 16;\n  //\n  int64 last_play_progress_time = 17;\n  //\n  int64 max_play_progress_time = 18;\n  //\n  int32 from = 19;\n  //\n  string from_spmid = 20;\n  //\n  string spmid = 21;\n  //\n  string epid_status = 22;\n  //\n  string play_status = 23;\n  //\n  string user_status = 24;\n  //\n  int64 actual_played_time = 25;\n  //\n  int32 auto_play = 26;\n  //\n  int64 list_play_time = 27;\n  //\n  int64 detail_play_time = 28;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/api/probe/v1/probe.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.api.probe.v1;\n\noption java_multiple_files = true;\n\n// 服务可用性探针\nservice Probe {\n  //\n  rpc TestCode (CodeReq) returns (CodeReply);\n  //\n  rpc TestReq (ProbeReq) returns (ProbeReply);\n  //\n  rpc TestStream (ProbeStreamReq) returns (ProbeStreamReply);\n  //\n  rpc TestSub (ProbeSubReq) returns (ProbeSubReply);\n}\n\n// 服务可用性探针\nservice ProbeService {\n  //\n  rpc Echo(SimpleMessage) returns (SimpleMessage);\n  //\n  rpc EchoBody(SimpleMessage) returns (SimpleMessage);\n  //\n  rpc EchoDelete(SimpleMessage) returns (SimpleMessage);\n  //\n  rpc EchoError(ErrorMessage) returns (ErrorMessage);\n  //\n  rpc EchoPatch(DynamicMessageUpdate) returns (DynamicMessageUpdate);\n}\n\n//\nenum Category {\n  CATEGORY_UNSPECIFIED = 0; //\n  CATEGORY_ONE = 1; //\n  CATEGORY_TWO = 2; //\n  CATEGORY_THREE = 3; //\n  CATEGORY_FOUR = 4; //\n}\n\n//\nmessage CodeReply {\n  //\n  string id = 1;\n  //\n  string id1 = 2;\n  //\n  int64 code = 3;\n  //\n  string message_s = 4;\n}\n\n//\nmessage CodeReq {\n  //\n  int64 code = 1;\n}\n\n// \nmessage CreateTopic {\n  //\n  int64 id = 1;\n}\n\n// \nmessage CreatTask {\n  //\n  string task = 1;\n}\n\n//\nmessage DynamicMessageUpdate {\n  //\n  SimpleMessage body = 1;\n}\n\n//\nmessage Embedded {\n  //\n  bool bool_val = 1;\n  //\n  int32 int32_val = 2;\n  //\n  int64 int64_val = 3;\n  //\n  float float_val = 4;\n  //\n  double double_val = 5;\n  //\n  string string_val = 6;\n  //\n  repeated bool repeated_bool_val = 7;\n  //\n  repeated int32 repeated_int32_val = 8;\n  //\n  repeated int64 repeated_int64_val = 9;\n  //\n  repeated float repeated_float_val = 10;\n  //\n  repeated double repeated_double_val = 11;\n  //\n  repeated string repeated_string_val = 12;\n  //\n  map<string, string> map_string_val = 13;\n  //\n  map<string, ErrorMessage> map_error_val = 14;\n}\n\n//\nmessage ErrorMessage {\n  //\n  int64 code = 1;\n  //\n  string reason = 2;\n  //\n  string message = 3;\n}\n\n// Deprecated\nenum ErrorReason {\n  PROBE_UNSPECIFIED = 0; //\n  PROBE_CATEGORY_NOTFOUND = 1; //\n}\n\n//\nmessage ProbeReply {\n  //\n  string content = 1;\n  //\n  int64 timestamp = 2;\n}\n\n//\nmessage ProbeReq {\n  //\n  int64 mid = 1;\n  //\n  string buvid = 2;\n}\n\n//\nmessage ProbeStreamReply {\n  //\n  int64 sequence = 1;\n  //\n  int64 timestamp = 2;\n  //\n  string content = 3;\n}\n\n//\nmessage ProbeStreamReq {\n  //\n  int64 mid = 1;\n  //\n  int64 sequence = 2;\n}\n\n//\nmessage ProbeSubReply {\n  //\n  int64 message_id = 1;\n}\n\n//\nmessage ProbeSubReq {\n  //\n  string buvid = 1;\n}\n\n//\nmessage SimpleMessage {\n  //\n  int32 id = 1;\n  //\n  int64 num = 2;\n  //\n  string lang = 3;\n  //\n  int32 cate = 4;\n  //\n  Embedded embedded = 5;\n}\n\n// \nmessage Task {\n  //\n  string name = 1;\n  //\n  string author = 2;\n  //\n  bool cache = 3;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/api/ticket/v1/ticket.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.api.ticket.v1;\n\noption java_multiple_files = true;\n\nservice Ticket {\n  // 获取鉴权用 Ticket\n  rpc GetTicket (GetTicketRequest) returns (GetTicketResponse);\n}\n\n//\nmessage GetTicketRequest {\n  // 包含:\n  // + x-fingerprint(包含手机各类信息, 使用 datacenter.hakase.protobuf.AndroidDeviceInfo 生成)\n  // + x-exbadbasket(?)\n  map<string, bytes> context = 1;\n  // 暂时固定为 ec01\n  string key_id = 2;\n  //\n  bytes sign = 3;\n  // 暂时留空\n  string token = 4;\n}\n\n//\nmessage GetTicketResponse {\n  //\n  message Context {\n    //\n    string v_voucher = 1;\n  }\n  // x-bili-ticket\n  string ticket = 1;\n  // 有效期起\n  int64 created_at = 2;\n  // 有效期\n  int64 ttl = 3;\n  //\n  Context context = 4;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/archive/middleware/v1/preload.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.archive.middleware.v1;\n\noption java_multiple_files = true;\n\n// 视频秒开参数\nmessage PlayerArgs {\n  // 清晰度\n  int64 qn = 1;\n  // 流版本\n  int64 fnver = 2;\n  // 流类型\n  int64 fnval = 3;\n  // 返回url是否强制使用域名\n  // 0:不强制使用域名 1:http域名 2:https域名\n  int64 force_host = 4;\n  // 音量均衡\n  int64 voice_balance = 5;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/archive/v1/archive.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.archive.v1;\n\noption java_multiple_files = true;\n\n// 稿件基本信息\nmessage Arc {\n  // 稿件avid\n  int64 aid = 1;\n  // 稿件分P数\n  int64 videos = 2;\n  // 分区id\n  int32 type_id = 3;\n  // 二级分区名\n  string type_name = 4;\n  // 稿件类型\n  // 1:原创 2:转载\n  int32 copyright = 5;\n  // 稿件封面url\n  string pic = 6;\n  // 稿件标题\n  string title = 7;\n  // 稿件发布时间\n  int64 pubdate = 8;\n  // 用户投稿时间\n  int64 ctime = 9;\n  // 稿件简介\n  string desc = 10;\n  // 稿件状态\n  int32 state = 11;\n  // 访问属性\n  // 0:全部可见 10000:登录可见\n  int32 access = 12;\n  // 属性位配置(现在无了)\n  int32 attribute = 13;\n  // 空\n  string tag = 14;\n  // 空\n  repeated string tags = 15;\n  // 稿件总时长(单位为秒)\n  int64 duration = 16;\n  // 参与的活动id\n  int64 mission_id = 17;\n  // 绑定的商单id\n  int64 order_id = 18;\n  // PGC稿件强制重定向url(如番剧、影视)\n  string redirect_url = 19;\n  // 空\n  int64 forward = 20;\n  // 控制标志\n  Rights rights = 21;\n  // UP主信息\n  Author author = 22;\n  // 状态数\n  Stat stat = 23;\n  // 空\n  string report_result = 24;\n  // 投稿时发送的动态内容\n  string dynamic = 25;\n  // 稿件1P cid\n  int64 first_cid = 26;\n  // 稿件1P 分辨率\n  Dimension dimension = 27;\n  // 合作组成员列表\n  repeated StaffInfo staff_info = 28;\n  // UGC合集id\n  int64 season_id = 29;\n  // 新版属性位配置(也没用)\n  int64 attribute_v2 = 30;\n  //\n  SeasonTheme season_theme = 31;\n  //\n  string short_link_v2 = 40;\n  //\n  int32 up_from_v2 = 41;\n  //\n  string first_frame = 42;\n}\n\n// UP主信息\nmessage Author {\n  // UP主mid\n  int64 mid = 1;\n  // UP主昵称\n  string name = 2;\n  // UP主头像url\n  string face = 3;\n}\n\n// 分辨率\nmessage Dimension {\n  // 宽度\n  int64 width = 1;\n  // 高度\n  int64 height = 2;\n  // 方向\n  // 0:横屏 1:竖屏\n  int64 rotate = 3;\n}\n\n// 分P信息\nmessage Page {\n  // 视频cid\n  int64 cid = 1;\n  // 分P序号\n  int32 page = 2;\n  // 源类型\n  // vupload:B站 qq:腾讯 hunan:芒果\n  string from = 3;\n  // 分P标题\n  string part = 4;\n  // 分P时长(单位为秒)\n  int64 duration = 5;\n  // 外链vid\n  string vid = 6;\n  // 分P简介\n  string desc = 7;\n  // 外链url\n  string webLink = 8;\n  // 分P分辨率\n  Dimension dimension = 9;\n  //\n  string first_frame = 10;\n}\n\n// 稿件控制标志\nmessage Rights {\n  // 老版是否付费\n  int32 bp = 1;\n  // 允许充电\n  int32 elec = 2;\n  // 允许下载\n  int32 download = 3;\n  // 是否电影\n  int32 movie = 4;\n  // PGC稿件需要付费\n  int32 pay = 5;\n  // 是否高码率\n  int32 hd5 = 6;\n  // 是否显示“禁止转载”标志\n  int32 no_reprint = 7;\n  // 是否允许自动播放\n  int32 autoplay = 8;\n  // UGC稿件需要付费(旧版)\n  int32 ugc_pay = 9;\n  // 是否联合投稿\n  int32 is_cooperation = 10;\n  // 是否UGC付费预览\n  int32 ugc_pay_preview = 11;\n  // 是否禁止后台播放\n  int32 no_background = 12;\n  // UGC稿件需要付费\n  int32 arc_pay = 13;\n  // 是否已付费可自由观看\n  int32 pay_free_watch = 14;\n}\n\n//\nmessage SeasonTheme {\n  //\n  string bg_color = 1;\n  //\n  string selected_bg_color = 2;\n  //\n  string text_color = 3;\n}\n\n// 合作成员信息\nmessage StaffInfo {\n  // 成员mid\n  int64 mid = 1;\n  // 成员角色\n  string title = 2;\n  // 属性位\n  // 0:普通 1:赞助商金色标志\n  int64 attribute = 3;\n}\n\n// 状态数\nmessage Stat {\n  // 稿件avid\n  int64 aid = 1;\n  // 播放数(当屏蔽时为-1)\n  int64 view = 2;\n  // 弹幕数\n  int32 danmaku = 3;\n  // 评论数\n  int32 reply = 4;\n  // 收藏数\n  int32 fav = 5;\n  // 投币数\n  int32 coin = 6;\n  // 分享数\n  int32 share = 7;\n  // 当前排名\n  int32 now_rank = 8;\n  // 历史最高排名\n  int32 his_rank = 9;\n  // 点赞数\n  int32 like = 10;\n  // 点踩数(前端不可见故恒为0)\n  int32 dislike = 11;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/card/v1/ad.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.card.v1;\n\noption java_multiple_files = true;\n\n//\nmessage AdInfo {\n  //\n  int64 creative_id = 1;\n  //\n  int32 creative_type = 2;\n  //\n  int32 card_type = 3;\n  //\n  CreativeContent creative_content = 4;\n  //\n  string ad_cb = 5;\n  //\n  int64 resource = 6;\n  //\n  int32 source = 7;\n  //\n  string request_id = 8;\n  //\n  bool is_ad = 9;\n  //\n  int64 cm_mark = 10;\n  //\n  int32 index = 11;\n  //\n  bool is_ad_loc = 12;\n  //\n  int32 card_index = 13;\n  //\n  string client_ip = 14;\n  //\n  bytes extra = 15;\n  //\n  int32 creative_style = 16;\n}\n\n//\nmessage CreativeContent {\n  //\n  string title = 1;\n  //\n  string description = 2;\n  //\n  int64 video_id = 3;\n  //\n  string username = 4;\n  //\n  string image_url = 5;\n  //\n  string image_md5 = 6;\n  //\n  string log_url = 7;\n  //\n  string log_md5 = 8;\n  //\n  string url = 9;\n  //\n  string click_url = 10;\n  //\n  string show_url = 11;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/card/v1/card.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.card.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/card/v1/single.proto\";\n\n// 卡片信息\nmessage Card {\n  oneof item {\n    // 小封面条目\n    SmallCoverV5 small_cover_v5 = 1;\n    //\n    LargeCoverV1 large_cover_v1 = 2;\n    //\n    ThreeItemAllV2 three_item_all_v2 = 3;\n    //\n    ThreeItemV1 three_item_v1 = 4;\n    //\n    HotTopic hot_topic = 5;\n    //\n    DynamicHot three_item_h_v5 = 6;\n    //\n    MiddleCoverV3 middle_cover_v3 = 7;\n    //\n    LargeCoverV4 large_cover_v4 = 8;\n    // 热门列表顶部按钮\n    PopularTopEntrance popular_top_entrance = 9;\n    //\n    RcmdOneItem rcmd_one_item = 10;\n    //\n    SmallCoverV5Ad small_cover_v5_ad = 11;\n  }\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/card/v1/common.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.card.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/card/v1/ad.proto\";\n\n//\nmessage Args {\n  //\n  int32 type = 1;\n  //\n  int64 up_id = 2;\n  //\n  string up_name = 3;\n  //\n  int32 rid = 4;\n  //\n  string rname = 5;\n  //\n  int64 tid = 6;\n  //\n  string tname = 7;\n  //\n  string track_id = 8;\n  //\n  string state = 9;\n  //\n  int32 converge_type = 10;\n  //\n  int64 aid = 11;\n}\n\n//\nmessage Avatar {\n  //\n  string cover = 1;\n  //\n  string text = 2;\n  //\n  string uri = 3;\n  //\n  int32 type = 4;\n  //\n  string event = 5;\n  //\n  string event_v2 = 6;\n  //\n  int32 defalut_cover = 7;\n}\n\n// 条目基本信息\nmessage Base {\n  // 卡片类型\n  string card_type = 1;\n  // 卡片跳转类型?\n  string card_goto = 2;\n  // 跳转类型\n  // av:视频稿件 mid:用户空间\n  string goto = 3;\n  // 目标参数\n  string param = 4;\n  // 封面url\n  string cover = 5;\n  // 标题\n  string title = 6;\n  // 跳转uri\n  string uri = 7;\n  //\n  ThreePoint three_point = 8;\n  //\n  Args args = 9;\n  //\n  PlayerArgs player_args = 10;\n  // 条目排位序号\n  int64 idx = 11;\n  //\n  AdInfo ad_info = 12;\n  //\n  Mask mask = 13;\n  //来源标识\n  // recommend:推荐 operation:管理?\n  string from_type = 14;\n  //\n  repeated ThreePointV2 three_point_v2 = 15;\n  //\n  repeated ThreePointV3 three_point_v3 = 16;\n  //\n  Button desc_button = 17;\n  // 三点v4\n  ThreePointV4 three_point_v4 = 18;\n  //\n  UpArgs up_args = 19;\n}\n\n// 按钮信息\nmessage Button {\n  // 文案\n  string text = 1;\n  // 参数\n  string param = 2;\n  //\n  string uri = 3;\n  // 事件\n  string event = 4;\n  //\n  int32 selected = 5;\n  // 类型\n  int32 type = 6;\n  // 事件v2\n  string event_v2 = 7;\n  // 关系信息\n  Relation relation = 8;\n}\n\n//\nmessage DislikeReason {\n  //\n  int64 id = 1;\n  //\n  string name = 2;\n}\n\n//\nmessage LikeButton {\n  //\n  int64 Aid = 1;\n  //\n  int32 count = 2;\n  //\n  bool show_count = 3;\n  //\n  string event = 4;\n  //\n  int32 selected = 5;\n  //\n  string event_v2 = 6;\n}\n\n//\nmessage Mask {\n  //\n  Avatar avatar = 1;\n  //\n  Button button = 2;\n}\n\n//\nmessage PlayerArgs {\n  //\n  int32 is_live = 1;\n  //\n  int64 aid = 2;\n  //\n  int64 cid = 3;\n  //\n  int32 sub_type = 4;\n  //\n  int64 room_id = 5;\n  //\n  int64 ep_id = 7;\n  //\n  int32 is_preview = 8;\n  //\n  string type = 9;\n  //\n  int64 duration = 10;\n  //\n  int64 season_id = 11;\n}\n\n// 标签框信息\nmessage ReasonStyle {\n  // 文案\n  string text = 1;\n  // 文字颜色\n  string text_color = 2;\n  // 背景色\n  string bg_color = 3;\n  // 边框色\n  string border_color = 4;\n  // 图标url\n  string icon_url = 5;\n  // 文字颜色-夜间\n  string text_color_night = 6;\n  // 背景色-夜间\n  string bg_color_night = 7;\n  // 边框色-夜间\n  string border_color_night = 8;\n  // 图标url-夜间\n  string icon_night_url = 9;\n  // 背景风格id\n  // 1:无背景 2:有背景\n  int32 bg_style = 10;\n  //\n  string uri = 11;\n  //\n  string icon_bg_url = 12;\n  //\n  string event = 13;\n  //\n  string event_v2 = 14;\n  //\n  int32 right_icon_type = 15;\n  //\n  string left_icon_type = 16;\n}\n\n// 关系信息\nmessage Relation {\n  // 关系状态\n  int32 status = 1;\n  // 是否关注\n  int32 is_follow = 2;\n  // 是否粉丝\n  int32 is_followed = 3;\n}\n\n// 分享面板信息\nmessage SharePlane {\n  // 标题\n  string title = 1;\n  // 副标贴文案\n  string share_subtitle = 2;\n  // 备注\n  string desc = 3;\n  // 封面url\n  string cover = 4;\n  // 稿件avid\n  int64 aid = 5;\n  // 稿件bvid\n  string bvid = 6;\n  // 允许分享方式\n  map<string, bool> share_to = 7;\n  // UP主昵称\n  string author = 8;\n  // UP主mid\n  int64 author_id = 9;\n  // 短连接\n  string short_link = 10;\n  // 播放次数文案\n  string play_number = 11;\n  //\n  int64 first_cid = 12;\n}\n\n//\nmessage ThreePoint {\n  //\n  repeated DislikeReason dislike_reasons = 1;\n  //\n  repeated DislikeReason feedbacks = 2;\n  //稍后再看\n  int32 watch_later = 3;\n}\n\n//\nmessage ThreePointV2 {\n  //\n  string title = 1;\n  //\n  string subtitle = 2;\n  //\n  repeated DislikeReason reasons = 3;\n  //\n  string type = 4;\n  //\n  int64 id = 5;\n}\n\n//\nmessage ThreePointV3 {\n  //\n  string title = 1;\n  //\n  string selected_title = 2;\n  //\n  string subtitle = 3;\n  //\n  repeated DislikeReason reasons = 4;\n  //\n  string type = 5;\n  //\n  int64 id = 6;\n  //\n  int32 selected = 7;\n  //\n  string icon = 8;\n  //\n  string selected_icon = 9;\n  //\n  string url = 10;\n  //\n  int32 default_id = 11;\n}\n\n// 三点v4\nmessage ThreePointV4 {\n  // 分享面板信息\n  SharePlane share_plane = 1;\n  // 稍后再看\n  WatchLater watch_later = 2;\n}\n\n//\nmessage Up {\n  //\n  int64 id = 1;\n  //\n  string name = 2;\n  //\n  string desc = 3;\n  //\n  Avatar avatar = 4;\n  //\n  int32 official_icon = 5;\n  //\n  Button desc_button = 6;\n  //\n  string cooperation = 7;\n}\n\n//\nmessage UpArgs {\n  //\n  int64 up_id = 1;\n  //\n  string up_name = 2;\n  //\n  string up_face = 3;\n  //\n  int64 selected = 4;\n}\n\n// 稍后再看信息\nmessage WatchLater {\n  // 稿件avid\n  int64 aid = 1;\n  // 稿件bvid\n  string bvid = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/card/v1/double.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.card.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/card/v1/common.proto\";\n\n//\nmessage DoubleCards {\n  oneof Card {\n    //\n    SmallCoverV2 small_cover_v2 = 1;\n    //\n    OnePicV2 one_pic_v2 = 2;\n    //\n    ThreePicV2 three_pic_v2 = 3;\n  }\n}\n\n//\nmessage SmallCoverV2 {\n  //\n  Base base = 1;\n  //\n  string cover_gif = 2;\n  //\n  int32 cover_blur = 3;\n  //\n  string cover_left_text_1 = 4;\n  //\n  int32 cover_left_icon_1 = 5;\n  //\n  string cover_left_text_2 = 6;\n  //\n  int32 cover_left_icon_2 = 7;\n  //\n  string cover_right_text = 8;\n  //\n  int32 cover_right_icon = 9;\n  //\n  string cover_right_background_color = 10;\n  //\n  string subtitle = 11;\n  //\n  string badge = 12;\n  //\n  string rcmd_reason = 13;\n  //\n  string desc = 14;\n  //\n  Avatar avatar = 15;\n  //\n  int32 official_icon = 16;\n  //\n  int32 can_play = 17;\n  //\n  ReasonStyle rcmd_reason_style = 18;\n  //\n  ReasonStyle rcmd_reason_style_v2 = 19;\n  //\n  LikeButton like_button = 20;\n}\n\n//\nmessage SmallCoverV3 {\n  //\n  Base base = 1;\n  //\n  Avatar avatar = 2;\n  //\n  string cover_left_text = 3;\n  //\n  Button cover_right_button = 4;\n  //\n  string rcmd_reason = 5;\n  //\n  string desc = 6;\n  //\n  int32 official_icon = 7;\n  //\n  int32 can_play = 8;\n  //\n  ReasonStyle rcmd_reason_style = 9;\n}\n\n//\nmessage MiddleCoverV2 {\n  //\n  Base base = 1;\n  //\n  int32 ratio = 2;\n  //\n  string desc = 3;\n  //\n  string badge = 4;\n}\n\n//\nmessage LargeCoverV2 {\n  //\n  Base base = 1;\n  //\n  Avatar avatar = 2;\n  //\n  string badge = 3;\n  //\n  Button cover_right_button = 4;\n  //\n  string cover_left_text_1 = 5;\n  //\n  int32 cover_left_icon_1 = 6;\n  //\n  string cover_left_text_2 = 7;\n  //\n  int32 cover_left_icon_2 = 8;\n  //\n  string rcmd_reason = 9;\n  //\n  int32 official_icon = 10;\n  //\n  int32 can_play = 11;\n  //\n  ReasonStyle rcmd_reason_style = 12;\n  //\n  int32 show_top = 13;\n  //\n  int32 show_bottom = 14;\n}\n\n//\nmessage ThreeItemV2 {\n  //\n  Base base = 1;\n  //\n  int32 title_icon = 2;\n  //\n  string more_uri = 3;\n  //\n  string more_text = 4;\n  //\n  repeated ThreeItemV2Item items = 5;\n}\n\n//\nmessage ThreeItemV2Item {\n  //\n  Base base = 1;\n  //\n  int32 cover_left_icon = 2;\n  //\n  string desc_text_1 = 3;\n  //\n  int32 desc_icon_1 = 4;\n  //\n  string desc_text_2 = 5;\n  //\n  int32 desc_icon_2 = 6;\n  //\n  string badge = 7;\n}\n\n//\nmessage SmallCoverV4 {\n  //\n  Base base = 1;\n  //\n  string cover_badge = 2;\n  //\n  string desc = 3;\n  //\n  string title_right_text = 4;\n  //\n  int32 title_right_pic = 5;\n}\n\n//\nmessage TwoItemV2 {\n  //\n  Base base = 1;\n  //\n  repeated TwoItemV2Item items = 2;\n}\n\nmessage TwoItemV2Item {\n  //\n  Base base = 1;\n  //\n  string badge = 2;\n  //\n  string cover_left_text_1 = 3;\n  //\n  int32 cover_left_icon_1 = 4;\n}\n\n//\nmessage MultiItem {\n  //\n  Base base = 1;\n  //\n  string more_uri = 2;\n  //\n  string more_text = 3;\n  //\n  repeated DoubleCards items = 4;\n}\n\n//\nmessage ThreePicV2 {\n  //\n  Base base = 1;\n  //\n  string left_cover = 2;\n  //\n  string right_cover_1 = 3;\n  //\n  string right_cover_2 = 4;\n  //\n  string cover_left_text_1 = 5;\n  //\n  int32 cover_left_icon_1 = 6;\n  //\n  string cover_left_text_2 = 7;\n  //\n  int32 cover_left_icon_2 = 8;\n  //\n  string cover_right_text = 9;\n  //\n  int32 cover_right_icon = 10;\n  //\n  string cover_right_background_color = 11;\n  //\n  string badge = 12;\n  //\n  string rcmd_reason = 13;\n  //\n  string desc = 14;\n  //\n  Avatar avatar = 15;\n  //\n  ReasonStyle rcmd_reason_style = 16;\n}\n\n//\nmessage OnePicV2 {\n  //\n  Base base = 1;\n  //\n  int32 cover_left_icon_1 = 2;\n  //\n  string cover_left_text_2 = 3;\n  //\n  string cover_right_text = 4;\n  //\n  int32 cover_right_icon = 5;\n  //\n  string cover_right_background_color = 6;\n  //\n  string badge = 7;\n  //\n  string rcmd_reason = 8;\n  //\n  Avatar avatar = 9;\n  //\n  ReasonStyle rcmd_reason_style = 10;\n}\n\n//\nmessage LargeCoverV3 {\n  //\n  Base base = 1;\n  //\n  string cover_gif = 2;\n  //\n  Avatar avatar = 3;\n  //\n  ReasonStyle top_rcmd_reason_style = 4;\n  //\n  ReasonStyle bottom_rcmd_reason_style = 5;\n  //\n  string cover_left_text_1 = 6;\n  //\n  int32 cover_left_icon_1 = 7;\n  //\n  string cover_left_text_2 = 8;\n  //\n  int32 cover_left_icon_2 = 9;\n  //\n  string cover_right_text = 10;\n  //\n  string desc = 11;\n  //\n  int32 official_icon = 12;\n}\n\n//\nmessage ThreePicV3 {\n  //\n  Base base = 1;\n  //\n  string left_cover = 2;\n  //\n  string right_cover_1 = 3;\n  //\n  string right_cover_2 = 4;\n  //\n  string cover_left_text_1 = 5;\n  //\n  int32 cover_left_icon_1 = 6;\n  //\n  string cover_left_text_2 = 7;\n  //\n  int32 cover_left_icon_2 = 8;\n  //\n  string cover_right_text = 9;\n  //\n  int32 cover_right_icon = 10;\n  //\n  string cover_right_background_color = 11;\n  //\n  string badge = 12;\n  //\n  ReasonStyle rcmd_reason_style = 13;\n}\n\n//\nmessage OnePicV3 {\n  //\n  Base base = 1;\n  //\n  string cover_left_text_1 = 2;\n  //\n  int32 cover_left_icon_1 = 3;\n  //\n  string cover_right_text = 4;\n  //\n  int32 cover_right_icon = 5;\n  //\n  string cover_right_background_color = 6;\n  //\n  string badge = 7;\n  //\n  ReasonStyle rcmd_reason_style = 8;\n}\n\n//\nmessage SmallCoverV7 {\n  //\n  Base base = 1;\n  //\n  string desc = 2;\n}\n\n//\nmessage SmallCoverV9 {\n  //\n  Base base = 1;\n  //\n  string cover_left_text_1 = 2;\n  //\n  int32 cover_left_icon_1 = 3;\n  //\n  string cover_left_text_2 = 4;\n  //\n  int32 cover_left_icon_2 = 5;\n  //\n  string cover_right_text = 6;\n  //\n  int32 cover_right_icon = 7;\n  //\n  int32 can_play = 8;\n  //\n  ReasonStyle rcmd_reason_style = 9;\n  //\n  Up up = 10;\n  //\n  ReasonStyle left_cover_badge_style = 11;\n  //\n  ReasonStyle left_bottom_rcmd_reason_style = 12;\n}\n\n//\nmessage SmallCoverConvergeV2 {\n  //\n  Base base = 1;\n  //\n  string cover_left_text_1 = 2;\n  //\n  int32 cover_left_icon_1 = 3;\n  //\n  string cover_left_text_2 = 4;\n  //\n  int32 cover_left_icon_2 = 5;\n  //\n  string cover_right_text = 6;\n  //\n  string cover_right_top_text = 7;\n  //\n  ReasonStyle rcmd_reason_style = 8;\n  //\n  ReasonStyle rcmd_reason_style_v2 = 9;\n}\n\n//\nmessage SmallChannelSpecial {\n  //\n  Base base = 1;\n  //\n  string bg_cover = 2;\n  //\n  string desc_1 = 3;\n  //\n  string desc_2 = 4;\n  //\n  string badge = 5;\n  //\n  ReasonStyle rcmd_reason_style_2 = 6;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/card/v1/single.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.card.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/card/v1/common.proto\";\n\n//\nmessage SmallCoverV5 {\n  // 条目基本信息\n  Base base = 1;\n  //\n  string cover_gif = 2;\n  //\n  Up up = 3;\n  // 封面右下角标文案\n  string cover_right_text_1 = 4;\n  // 右侧文案1\n  string right_desc_1 = 5;\n  // 右侧文案2\n  string right_desc_2 = 6;\n  // 右侧推荐原因标签框\n  ReasonStyle rcmd_reason_style = 7;\n  //\n  HotwordEntrance hotword_entrance = 8;\n  // 直播小卡的角标\n  ReasonStyle corner_mark_style = 9;\n  // 右侧文案1图标id\n  int32 right_icon_1 = 10;\n  // 右侧文案2图标id\n  int32 right_icon_2 = 11;\n  // 左上角角标\n  ReasonStyle left_corner_mark_style = 12;\n  //\n  string cover_right_text_content_description = 13;\n  //\n  string right_desc1_content_description = 14;\n}\n\n//\nmessage SmallCoverV5Ad {\n  //\n  Base base = 1;\n  //\n  string cover_gif = 2;\n  //\n  Up up = 3;\n  //\n  string cover_right_text1 = 4;\n  //\n  string right_desc1 = 5;\n  //\n  string right_desc2 = 6;\n  //\n  ReasonStyle rcmd_reason_style = 7;\n  //\n  HotwordEntrance hotword_entrance = 8;\n  //\n  ReasonStyle corner_mark_style = 9;\n  //\n  int32 right_icon1 = 10;\n  //\n  int32 right_icon2 = 11;\n  //\n  ReasonStyle left_corner_mark_style = 12;\n  //\n  string cover_right_text_content_description = 13;\n  //\n  string right_desc1_content_description = 14;\n}\n\n//\nmessage HotwordEntrance {\n  //\n  int64 hotword_id = 1;\n  //\n  string hot_text = 2;\n  //\n  string h5_url = 3;\n  //\n  string icon = 4;\n}\n\n//\nmessage LargeCoverV1 {\n  // 条目基本信息\n  Base base = 1;\n  //\n  string cover_gif = 2;\n  //\n  Avatar avatar = 3;\n  //\n  string cover_left_text_1 = 4;\n  //\n  string cover_left_text_2 = 5;\n  //\n  string cover_left_text_3 = 6;\n  //\n  string cover_badge = 7;\n  //\n  string top_rcmd_reason = 8;\n  //\n  string bottom_rcmd_reason = 9;\n  //\n  string desc = 10;\n  //\n  int32 official_icon = 11;\n  //\n  int32 can_play = 12;\n  //\n  ReasonStyle top_rcmd_reason_style = 13;\n  //\n  ReasonStyle bottom_rcmd_reason_style = 14;\n  //\n  ReasonStyle rcmd_reason_style_v2 = 15;\n  //\n  ReasonStyle left_cover_badge_style = 16;\n  //\n  ReasonStyle right_cover_badge_style = 17;\n  //\n  string cover_badge_2 = 18;\n  //\n  LikeButton like_button = 19;\n  //\n  int32 title_single_line = 20;\n  //\n  string cover_right_text = 21;\n}\n\n//\nmessage ThreeItemAllV2 {\n  // 条目基本信息\n  Base base = 1;\n  //\n  ReasonStyle top_rcmd_reason_style = 2;\n  //\n  repeated TwoItemHV1Item item = 3;\n}\n\n//\nmessage TwoItemHV1Item {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string uri = 3;\n  //\n  string param = 4;\n  //\n  Args args = 5;\n  //\n  string goto = 6;\n  //\n  string cover_left_text_1 = 7;\n  //\n  int32 cover_left_icon_1 = 8;\n  //\n  string cover_right_text = 9;\n}\n\n// 推荐\nmessage RcmdOneItem {\n  // 条目基本信息\n  Base base = 1;\n  // 标签框信息\n  ReasonStyle topRcmdReasonStyle = 2;\n  // 小封面推荐内容信息\n  SmallCoverRcmdItem item = 3;\n}\n\n// 小封面推荐内容信息\nmessage SmallCoverRcmdItem {\n  // 标题\n  string title = 1;\n  // 封面url\n  string cover = 2;\n  // 跳转uri\n  string uri = 3;\n  // 参数\n  string param = 4;\n  // 跳转类型\n  // av:视频稿件\n  string goto = 5;\n  // 封面右下角标文案\n  string coverRightText1 = 6;\n  // 右侧文案1\n  string rightDesc1 = 7;\n  // 右侧文案2\n  string rightDesc2 = 8;\n  //\n  string coverGif = 9;\n  // 右侧文案1图标id\n  int32 rightIcon1 = 10;\n  // 右侧文案2图标id\n  int32 rightIcon2 = 11;\n  //\n  string cover_right_text_content_description = 12;\n  //\n  string right_desc1_content_description = 13;\n}\n\n//\nmessage ThreeItemV1 {\n  // 条目基本信息\n  Base base = 1;\n  //\n  int32 titleIcon = 2;\n  //\n  string moreUri = 3;\n  //\n  string moreText = 4;\n  //\n  repeated ThreeItemV1Item items = 5;\n}\n\n//\nmessage ThreeItemV1Item {\n  // 条目基本信息\n  Base base = 1;\n  //\n  string coverLeftText = 2;\n  //\n  int32 coverLeftIcon = 3;\n  //\n  string desc1 = 4;\n  //\n  string desc2 = 5;\n  //\n  string badge = 6;\n}\n\n//\nmessage HotTopicItem {\n  //\n  string cover = 1;\n  //\n  string uri = 2;\n  //\n  string param = 3;\n  //\n  string name = 4;\n}\n\n//\nmessage HotTopic {\n  // 条目基本信息\n  Base base = 1;\n  //\n  string desc = 2;\n  //\n  repeated HotTopicItem items = 3;\n}\n\n//\nmessage DynamicHot {\n  // 条目基本信息\n  Base base = 1;\n  //\n  string top_left_title = 2;\n  //\n  string desc1 = 3;\n  //\n  string desc2 = 4;\n  //\n  string more_uri = 5;\n  //\n  string more_text = 6;\n  //\n  repeated string covers = 7;\n  //\n  string cover_right_text = 8;\n  //\n  ReasonStyle top_rcmd_reason_style = 9;\n}\n\n//\nmessage MiddleCoverV3 {\n  // 条目基本信息\n  Base base = 1;\n  //\n  string desc1 = 2;\n  //\n  string desc2 = 3;\n  //\n  ReasonStyle cover_badge_style = 4;\n}\n\n//\nmessage LargeCoverV4 {\n  // 条目基本信息\n  Base base = 1;\n  //\n  string cover_left_text_1 = 2;\n  //\n  string cover_left_text_2 = 3;\n  //\n  string cover_left_text_3 = 4;\n  //\n  string cover_badge = 5;\n  //\n  int32 can_play = 6;\n  //\n  Up up = 7;\n  //\n  string short_link = 8;\n  //\n  string share_subtitle = 9;\n  //\n  string play_number = 10;\n  //\n  string bvid = 11;\n  //\n  string sub_param = 12;\n}\n\n// 热门列表顶部按钮\nmessage PopularTopEntrance {\n  // 条目基本信息\n  Base base = 1;\n  // 按钮项\n  repeated EntranceItem items = 2;\n}\n\n// 热门列表按钮信息\nmessage EntranceItem {\n  // 跳转类型\n  string goto = 1;\n  // 图标url\n  string icon = 2;\n  // 标题\n  string title = 3;\n  // 入口模块id\n  string module_id = 4;\n  // 跳转uri\n  string uri = 5;\n  // 入口id\n  int64 entrance_id = 6;\n  // 气泡信息\n  Bubble bubble = 7;\n  // 入口类型\n  // 1:代表分品类热门\n  int32 entrance_type = 8;\n}\n\n// 气泡信息\nmessage Bubble {\n  // 文案\n  string bubble_content = 1;\n  // 版本\n  int32 version = 2;\n  // 起始时间\n  int64 stime = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/click/v1/heartbeat.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.click.v1;\n\noption java_multiple_files = true;\n\nservice Click {\n\n}\n\n// 账户信息\nmessage AccountInfo {\n  //\n  uint64 mid = 1;\n}\n\n// \nmessage AppInfo {\n  //\n  string top_page_class = 1;\n  // 客户端首次启动时的毫秒时间戳\n  int64 ftime = 2;\n  //\n  string did = 3;\n}\n\n// 心跳补充信息\nmessage Extra {\n  //\n  string session = 1;\n  //\n  string refer = 2;\n}\n\nmessage HeartBeatReply {}\n\n// \nmessage HeartBeatReq {\n  //\n  string session_v2 = 1;\n  //\n  Stage stage = 2;\n  // 流加载失败timeout\n  uint64 stream_timeout = 3;\n  //\n  uint64 batch_frequency = 4;\n  //\n  float frequency = 5;\n  //\n  VideoMeta video_meta = 6;\n  //\n  AppInfo app_info = 7;\n  //\n  AccountInfo account_info = 8;\n  //\n  PreProcessResult pre_process_result = 9;\n  //\n  repeated PlayerStatus player_status = 10;\n  //\n  VideoInfo video_info = 11;\n}\n\n// \nmessage PlayerStatus {\n  //\n  float playback_rate = 1;\n  //\n  uint64 progress = 2;\n  //\n  PlayState play_state = 3;\n  //\n  bool is_buffering = 4;\n}\n\n// \nenum PlayState {\n  //\n  STATE_UNKNOWN = 0;\n  //\n  PREPARING = 1;\n  //\n  PREPARED = 2;\n  //\n  PLAYING = 3;\n  //\n  PAUSED = 4;\n  //\n  STOPPED = 5;\n  //\n  FAILED = 6;\n}\n\n// \nmessage PreProcessResult {\n  //\n  int64 vt = 1;\n}\n\n// \nenum Stage {\n  //\n  STAGE_UNKNOWN = 0;\n  //\n  START = 1;\n  //\n  END = 2;\n  //\n  SAMPLE = 3;\n}\n\n// \nmessage VideoInfo {\n  //\n  uint64 cid_duration = 1;\n}\n\n// \nmessage VideoMeta {\n  //\n  uint64 aid = 1;\n  //\n  uint64 cid = 2;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/setting/download.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.distribution.setting.download;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/distribution/v1/distribution.proto\";\n\n// \nmessage DownloadSettingsConfig {\n  //\n  bilibili.app.distribution.v1.BoolValue enable_download_auto_start = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/setting/dynamic.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.distribution.setting.dynamic;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/distribution/v1/distribution.proto\";\n\n// \nmessage DynamicAutoPlay {\n  //\n  bilibili.app.distribution.v1.Int64Value value = 1;\n}\n\n// \nmessage DynamicDeviceConfig {\n  //\n  DynamicAutoPlay auto_play = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/setting/experimental.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.distribution.setting.experimental;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/distribution/v1/distribution.proto\";\n\n// \nmessage DynamicSelect {\n  //\n  bilibili.app.distribution.v1.BoolValue fold = 1;\n}\n\n// \nmessage Exp {\n  //\n  bilibili.app.distribution.v1.Int64Value id = 1;\n  //\n  bilibili.app.distribution.v1.Int32Value bucket = 2;\n}\n\n// \nmessage ExperimentalConfig {\n  //\n  bilibili.app.distribution.v1.StringValue flag = 1;\n  //\n  repeated Exp exps = 2;\n}\n\n// \nmessage MultipleTusConfig {\n  //\n  TopLeft top_left = 1;\n  //\n  DynamicSelect dynamic_select = 2;\n}\n\n// APP首页头像跳转信息\nmessage TopLeft {\n  //\n  bilibili.app.distribution.v1.StringValue url = 1;\n  //\n  bilibili.app.distribution.v1.StringValue story_foreground_image = 2;\n  //\n  bilibili.app.distribution.v1.StringValue story_background_image = 3;\n  //\n  bilibili.app.distribution.v1.StringValue listen_foreground_image = 4;\n  //\n  bilibili.app.distribution.v1.StringValue listen_background_image = 5;\n  //\n  bilibili.app.distribution.v1.StringValue ios_story_foreground_image = 6;\n  //\n  bilibili.app.distribution.v1.StringValue ios_story_background_image = 7;\n  //\n  bilibili.app.distribution.v1.StringValue ios_listen_foreground_image = 8;\n  //\n  bilibili.app.distribution.v1.StringValue ios_listen_background_image = 9;\n  //\n  bilibili.app.distribution.v1.StringValue goto = 10;\n  //\n  bilibili.app.distribution.v1.StringValue url_v2 = 11;\n  //\n  bilibili.app.distribution.v1.Int64Value goto_v2 = 12;\n  //\n  bilibili.app.distribution.v1.StringValue badge = 13;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/setting/internaldevice.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.distribution.setting.internaldevice;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/distribution/v1/distribution.proto\";\n\n// \nmessage InternalDeviceConfig {\n  // 首次启动时间\n  bilibili.app.distribution.v1.Int64Value fts = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/setting/night.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.distribution.setting.night;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/distribution/v1/distribution.proto\";\n\n// \nmessage NightSettingsConfig {\n  //\n  bilibili.app.distribution.v1.BoolValue is_night_follow_system = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/setting/other.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.distribution.setting.other;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/distribution/v1/distribution.proto\";\n\n// \nmessage OtherSettingsConfig {\n  //\n  bilibili.app.distribution.v1.Int64Value watermark_type = 1;\n  //\n  bilibili.app.distribution.v1.Int64Value web_image_quality_type = 2;\n  //\n  bilibili.app.distribution.v1.BoolValue enable_read_pasteboard = 3;\n  //\n  bilibili.app.distribution.v1.BoolValue paste_auto_jump = 4;\n  //\n  bilibili.app.distribution.v1.BoolValue mini_screen_play_when_back = 5;\n  //\n  bilibili.app.distribution.v1.BoolValue enable_resume_playing = 6;\n  //\n  bilibili.app.distribution.v1.BoolValue enable_wifi_auto_update = 7;\n  //\n  bilibili.app.distribution.v1.BoolValue enable_guide_screenshot_share = 8;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/setting/pegasus.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.distribution.setting.pegasus;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/distribution/v1/distribution.proto\";\n\n// \nmessage FeedModeValue {\n  //\n  bilibili.app.distribution.v1.Int64Value value = 1;\n}\n\n// \nmessage PegasusAutoPlay {\n  //\n  bilibili.app.distribution.v1.Int64Value single = 1;\n  //\n  bilibili.app.distribution.v1.Int64Value double = 2;\n  //\n  bilibili.app.distribution.v1.BoolValue single_affected_by_server_side = 3;\n  //\n  bilibili.app.distribution.v1.BoolValue double_affected_by_server_side = 4;\n}\n\n// \nmessage PegasusColumnValue {\n  //\n  bilibili.app.distribution.v1.Int64Value value = 1;\n  //\n  bilibili.app.distribution.v1.BoolValue affected_by_server_side = 2;\n}\n\n// \nmessage PegasusDeviceConfig {\n  //\n  PegasusColumnValue column = 1;\n  //\n  FeedModeValue mode = 2;\n  //\n  PegasusAutoPlay auto_play = 3;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/setting/play.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.distribution.setting.play;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/distribution/v1/distribution.proto\";\n\n// 云端保存的播放器配置\nmessage CloudPlayConfig {\n  // 启用杜比全景声\n  bilibili.app.distribution.v1.BoolValue enable_panorama = 1;\n  // 启用杜比音效\n  bilibili.app.distribution.v1.BoolValue enable_dolby = 2;\n  // 启用震动\n  bilibili.app.distribution.v1.BoolValue enable_shake = 3;\n  // 启用后台播放\n  bilibili.app.distribution.v1.BoolValue enable_background = 4;\n  // 启用HIRES\n  bilibili.app.distribution.v1.BoolValue enable_loss_less = 5;\n}\n\n// 播放器策略配置\nmessage PlayConfig {\n  //\n  bilibili.app.distribution.v1.BoolValue should_auto_play = 1;\n  //\n  bilibili.app.distribution.v1.BoolValue should_auto_fullscreen = 2;\n  //\n  bilibili.app.distribution.v1.BoolValue enable_playurl_https = 3;\n  //\n  bilibili.app.distribution.v1.BoolValue enable_danmaku_interaction = 4;\n  //\n  bilibili.app.distribution.v1.Int64Value small_screen_status = 5;\n  //\n  bilibili.app.distribution.v1.Int64Value player_codec_mode_key = 6;\n  //\n  bilibili.app.distribution.v1.BoolValue enable_gravity_rotate_screen = 7;\n  //\n  bilibili.app.distribution.v1.BoolValue enable_danmaku_monospaced = 8;\n  //\n  bilibili.app.distribution.v1.BoolValue enable_edit_subtitle = 9;\n  //\n  bilibili.app.distribution.v1.BoolValue enable_subtitle = 10;\n  //\n  bilibili.app.distribution.v1.Int64Value color_filter = 11;\n  //\n  bilibili.app.distribution.v1.BoolValue should_auto_story = 12;\n  //\n  bilibili.app.distribution.v1.BoolValue landscape_auto_story = 13;\n  //\n  bilibili.app.distribution.v1.BoolValue volume_balance = 14;\n}\n\n// 灰度测试特殊功能？ \nmessage SpecificPlayConfig {\n  //\n  bilibili.app.distribution.v1.BoolValue enable_segmented_section = 1;\n}\n\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/setting/privacy.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.distribution.setting.privacy;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/distribution/v1/distribution.proto\";\n\n// \nmessage MidPrivacySettingsConfig {\n  //\n  bilibili.app.distribution.v1.BoolValue recommend_to_known = 1;\n}\n\n// \nmessage PrivacySettingsConfig {\n  //\n  bilibili.app.distribution.v1.BoolValue ad_recommand_store = 1;\n  // 传感器权限\n  bilibili.app.distribution.v1.BoolValue sensor_access = 2;\n\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/setting/search.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.distribution.setting.search;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/distribution/v1/distribution.proto\";\n\n// \nmessage SearchAutoPlay {\n  //\n  bilibili.app.distribution.v1.Int64Value value = 1;\n  //\n  bilibili.app.distribution.v1.BoolValue affected_by_server_side = 2;\n}\n\n// \nmessage SearchDeviceConfig {\n  //\n  SearchAutoPlay auto_play = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/distribution/v1/distribution.proto",
    "content": "syntax = \"proto3\";\n\nimport \"google/protobuf/any.proto\";\n\npackage bilibili.app.distribution.v1;\n\noption java_multiple_files = true;\n\n// APP配置\nservice Distribution {\n  // 获取云端储存的用户偏好\n  rpc GetUserPreference (GetUserPreferenceReq) returns (GetUserPreferenceReply);\n  // 设定用户偏好\n  rpc SetUserPreference (SetUserPreferenceReq) returns (SetUserPreferenceReply);\n  // 获取云控配置\n  rpc UserPreference (UserPreferenceReq) returns (UserPreferenceReply);\n}\n\n// \nmessage GetUserPreferenceReq {\n  //\n  repeated string type_url = 1;\n  //\n  map<string, string> extra_context = 2;\n}\n\n// \nmessage GetUserPreferenceReply {\n  // 对应 GetUserPreferenceReq 的请求的类型\n  repeated google.protobuf.Any value = 1;\n}\n\n// \nmessage SetUserPreferenceReq {\n  //\n  repeated google.protobuf.Any preference = 1;\n  //\n  map<string, string> extra_context = 2;\n}\n\n// \nmessage SetUserPreferenceReply {}\n\n// \nmessage UserPreferenceReq {}\n\n// 云控配置下发\nmessage UserPreferenceReply {\n  // 具体解码需要根据实际请求 type_url 来判断\n  repeated google.protobuf.Any preference = 1;\n}\n\n// \nmessage BoolValue {\n  //\n  bool value = 1;\n  //\n  int64 last_modified = 2;\n  //\n  bool default_value = 3;\n  //\n  string exp = 4;\n}\n\n// \nmessage BytesValue {\n  //\n  bytes value = 1;\n  //\n  int64 last_modified = 2;\n  //\n  bytes default_value = 3;\n  //\n  string exp = 4;\n}\n\n// \nmessage DoubleValue {\n  //\n  double value = 1;\n  //\n  int64 last_modified = 2;\n  //\n  double default_value = 3;\n  //\n  string exp = 4;\n}\n\n// \nmessage FloatValue {\n  //\n  float value = 1;\n  //\n  int64 last_modified = 2;\n  //\n  float default_value = 3;\n  //\n  string exp = 4;\n}\n\n// \nmessage Int32Value {\n  //\n  int32 value = 1;\n  //\n  int64 last_modified = 2;\n  //\n  int32 default_value = 3;\n  //\n  string exp = 4;\n}\n\n// \nmessage Int64Value {\n  //\n  int64 value = 1;\n  //\n  int64 last_modified = 2;\n  //\n  int64 default_value = 3;\n  //\n  string exp = 4;\n}\n\n// \nmessage StringValue {\n  //\n  string value = 1;\n  //\n  int64 last_modified = 2;\n  //\n  string default_value = 3;\n  //\n  string exp = 4;\n}\n\n// \nmessage UInt32Value {\n  //\n  uint32 value = 1;\n  //\n  int64 last_modified = 2;\n  //\n  uint32 default_value = 3;\n  //\n  string exp = 4;\n}\n\n// \nmessage UInt64Value {\n  //\n  uint64 value = 1;\n  //\n  int64 last_modified = 2;\n  //\n  uint64 default_value = 3;\n  //\n  string exp = 4;\n}\n\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/dynamic/common/dynamic.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.dynamic.common;\n\noption java_multiple_files = true;\n\n//\nmessage ItemWHRatio {\n  //\n  int32 ratio = 1;\n  //\n  int32 width = 2;\n  //\n  int32 height = 3;\n}\n\n//\nenum WHRatio {\n  W_H_RATIO_1_1 = 0;\n  W_H_RATIO_16_9 = 1;\n  W_H_RATIO_3_4 = 2;\n  W_H_RATIO_CUSTOM = 3;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/dynamic/v1/dynamic.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.dynamic.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\n\n// v1动态\nservice Dynamic {\n  // 动态视频页\n  rpc DynVideo (DynVideoReq) returns (DynVideoReqReply);\n  // 批量动态id获取动态详情\n  rpc DynDetails (DynDetailsReq) returns (DynDetailsReply);\n  // 小视频连播页\n  rpc SVideo (SVideoReq) returns (SVideoReply);\n  // 动态tab页\n  rpc DynTab (DynTabReq) returns (DynTabReply);\n  // 同城接口开关\n  rpc DynOurCitySwitch (DynOurCitySwitchReq) returns (NoReply);\n  // 动态同城页\n  rpc DynOurCity(DynOurCityReq) returns (DynOurCityReply);\n  // 最近访问-个人视频feed流\n  rpc DynVideoPersonal(DynVideoPersonalReq) returns (DynVideoPersonalReply);\n  // 最近访问-标记已读\n  rpc DynUpdOffset(DynUpdOffsetReq) returns (NoReply);\n  // 动态红点接口\n  rpc DynRed(DynRedReq) returns(DynRedReply);\n  // 查看更多-列表\n  rpc DynMixUpListViewMore(NoReq) returns (DynMixUpListViewMoreReply);\n  // 查看更多-搜索\n  rpc DynMixUpListSearch(DynMixUpListSearchReq) returns (DynMixUpListSearchReply);\n  // 同城点击上报\n  rpc OurCityClickReport(OurCityClickReportReq) returns (OurCityClickReportReply);\n  // 位置定位\n  rpc GeoCoder(GeoCoderReq) returns (GeoCoderReply);\n}\n\n// 地址部件\nmessage AddressComponent {\n  // 国家\n  string nation = 1;\n  // 省\n  string province = 2;\n  // 市\n  string city = 3;\n  // 区，可能为空字串\n  string district = 4;\n  // 街道，可能为空字串\n  string street = 5;\n  // 门牌，可能为空字串\n  string street_number = 6;\n}\n\n// 行政区划信息\nmessage AdInfo {\n  // 国家代码(ISO3166标准3位数字码)\n  string nation_code = 1;\n  // 行政区划代码，规则详见：行政区划代码说明\n  string adcode = 2;\n  // 城市代码，由国家码+行政区划代码(提出城市级别)组合而来，总共为9位\n  string city_code = 3;\n  // 行政区划名称\n  string name = 4;\n  // 行政区划中心点坐标\n  Gps gps = 5;\n}\n\n//\nenum BgType {\n  bg_type_default = 0; //\n  bg_type_face = 1; //\n}\n\n// 付费课程批次卡\nmessage CardCurrBatch {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 跳转地址\n  string uri = 3;\n  // 展示项 1(本集标题)\n  string text_1 = 4;\n  // 展示项 2(更新了多少个视频)\n  string text_2 = 5;\n  // 角标\n  VideoBadge badge = 6;\n}\n\n// 付费课程系列卡\nmessage CardCurrSeason {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 跳转地址\n  string uri = 3;\n  // 展示项 1(更新信息)\n  string text_1 = 4;\n  // 描述信息\n  string desc = 5;\n  // 角标\n  VideoBadge badge = 6;\n}\n\n// PGC视频卡片数据\nmessage CardPGC {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 秒开地址\n  string uri = 3;\n  // 视频封面展示项 1\n  string cover_left_text_1 = 4;\n  // 视频封面展示项 2\n  string cover_left_text_2 = 5;\n  // 封面视频展示项 3\n  string cover_left_text_3 = 6;\n  // cid\n  int64 cid = 7;\n  // season_id\n  int64 season_id = 8;\n  // epid\n  int64 epid = 9;\n  // aid\n  int64 aid = 10;\n  // 视频源类型\n  MediaType media_type = 11;\n  // 番剧类型\n  VideoSubType sub_type = 12;\n  // 番剧是否为预览视频 0:否，1:是\n  int32 is_preview = 13;\n  // 尺寸信息\n  Dimension dimension = 14;\n  // 角标\n  repeated VideoBadge badge = 15;\n  // 是否能够自动播放\n  int32  can_play = 16;\n  // PGC单季信息\n  PGCSeason season = 17;\n}\n\n// UGC视频卡片数据\nmessage CardUGC {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 秒开地址\n  string uri = 3;\n  // 视频封面展示项 1\n  string cover_left_text_1 = 4;\n  // 视频封面展示项 2\n  string cover_left_text_2 = 5;\n  // 封面视频展示项 3\n  string cover_left_text_3 = 6;\n  // avid\n  int64 avid = 7;\n  // cid\n  int64 cid = 8;\n  // 视频源类型\n  MediaType media_type = 9;\n  // 尺寸信息\n  Dimension dimension = 10;\n  // 角标\n  repeated VideoBadge badge = 11;\n  // 是否能够自动播放\n  int32  can_play = 12;\n}\n\n//\nenum CornerType {\n  corner_type_none = 0;  //\n  corner_type_text = 1;  //\n  corner_type_animation = 2;  //\n}\n\n// 粉丝样式\nmessage DecoCardFan {\n  // 是否是粉丝\n  int32 is_fan = 1;\n  // 数量\n  int32 number = 2;\n  // 颜色\n  string color = 3;\n}\n\n// 装扮卡片\nmessage DecorateCard {\n  // 装扮卡片id\n  int64 id = 1;\n  // 装扮卡片链接\n  string card_url = 2;\n  // 装扮卡片点击跳转链接\n  string jump_url = 3;\n  // 粉丝样式\n  DecoCardFan fan = 4;\n}\n\n// 文本描述\nmessage Description {\n  // 文本内容\n  string text = 1;\n  // 文本类型\n  string type = 2;\n  // 点击跳转链接\n  string uri = 3;\n  // emoji类型\n  string emoji_type = 4;\n  // 商品类型\n  string goods_type = 5;\n}\n\n// 尺寸信息\nmessage Dimension {\n  //\n  int64 height = 1;\n  //\n  int64 width = 2;\n  //\n  int64 rotate = 3;\n}\n\n// 动态卡片项\nmessage DynamicItem {\n  // 卡片类型\n  // forward:转发 av:稿件视频 fold:折叠 pgc:pgc内容 courses:付费视频 upList:最近访问列表 followList:我的追番列表\n  string card_type = 1;\n  // 转发类型下，items的类型\n  string item_type = 2;\n  // 模块内容\n  repeated Module modules = 3;\n  // 动态ID str\n  string dyn_id_str = 4;\n  // 转发动态ID str\n  string orig_dyn_id_str = 5;\n  // r_type\n  int32 r_type = 6;\n  // 该卡片下面是否含有折叠卡\n  int32 has_fold = 7;\n}\n\n// 批量动态id获取动态详情返回值\nmessage DynDetailsReply {\n  // 动态列表\n  repeated DynamicItem list = 1;\n}\n\n// 批量动态id获取动态详情请求参数\nmessage DynDetailsReq {\n  // 青少年模式\n  int32 teenagers_mode = 1;\n  // 动态id\n  string dynamic_ids = 2;\n  // 清晰度\n  int32 qn = 3;\n  // 流版本\n  int32 fnver = 4;\n  // 流功能\n  int32 fnval = 5;\n  // 是否强制使用域名\n  int32 force_host = 6;\n  // 是否4k\n  int32 fourk = 7;\n}\n\n// 查看更多-搜索-响应\nmessage DynMixUpListSearchReply {\n  //\n  repeated MixUpListItem items = 1;\n}\n\n// 查看更多-搜索-请求\nmessage DynMixUpListSearchReq {\n  //\n  string name = 1;\n}\n\n// 查看更多-列表-响应\nmessage DynMixUpListViewMoreReply {\n  // 关注up主列表信息\n  repeated MixUpListItem items = 1;\n  // 默认搜索文案\n  string  search_default_text = 2;\n}\n\n// 动态同城物料\nmessage DynOurCityItem {\n  // 卡片类型\n  // av:稿件 draw:图文\n  string card_type = 1;\n  // 动态ID\n  int64 dyn_id = 2;\n  // 跳转地址\n  string uri = 3;\n  // 模块列表\n  repeated DynOurCityModule modules = 4;\n  // 资源ID\n  int64 rid = 5;\n  // 透传服务端魔镜参数\n  string debug_info = 6;\n}\n\n// 动态同城物料模块\nmessage DynOurCityModule {\n  // 类型\n  // cover:封面 desc:描述 author:发布人 extend:扩展部分\n  string module_type = 1;\n  //\n  oneof module_item {\n    // 封面\n    DynOurCityModuleCover module_cover = 2;\n    // 描述\n    DynOurCityModuleDesc module_desc = 3;\n    // 发布人\n    DynOurCityModuleAuthor module_author = 4;\n    // 扩展部分\n    DynOurCityModuleExtend module_extend = 5;\n  }\n}\n\n// 动态同城物料-发布人模块\nmessage DynOurCityModuleAuthor {\n  // 用户Mid\n  int64 mid = 1;\n  // 用户昵称\n  string name = 2;\n  // 用户头像\n  string face = 3;\n  // 跳转地址\n  string uri = 4;\n}\n\n// 动态同城物料-封面模块\nmessage DynOurCityModuleCover {\n  // 封面图 单图样式取第一个元素\n  repeated string covers = 1;\n  // 封面样式\n  // 1:横图 2:竖图 3:方图\n  int32 style = 2;\n  // 视频封面展示项图标 1\n  int32 cover_left_icon_1 = 3;\n  // 视频封面展示项 1\n  string cover_left_text_1 = 4;\n  // 视频封面展示项图标 2\n  int32 cover_left_icon_2 = 5;\n  // 视频封面展示项 2\n  string cover_left_text_2 = 6;\n  // 封面视频展示项 3\n  string cover_left_text_3 = 7;\n  // 角标\n  repeated VideoBadge badge = 8;\n}\n\n// 动态同城物料-描述模块\nmessage DynOurCityModuleDesc {\n  // 描述信息\n  string desc = 1;\n}\n\n// 动态同城物料-扩展部分模块\nmessage DynOurCityModuleExtend {\n  // 类型\n  string type = 1;\n  oneof extend {\n    // LBS模块\n    DynOurCityModuleExtendLBS extend_lbs = 2;\n  }\n}\n\n// 动态同城物料extent-LBS模块\nmessage DynOurCityModuleExtendLBS {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n  // poiType\n  int32 poi_type = 4;\n}\n\n// 动态同城-响应\nmessage DynOurCityReply {\n  // 翻页游标\n  string offset = 1;\n  // 是否还有更多数据\n  // 1:有\n  int32 has_more = 2;\n  // 样式类型\n  // 1:双列 2:瀑布流\n  int32 style = 3;\n  // 顶导信息\n  string top_label = 4;\n  // 列表详情\n  repeated DynOurCityItem list = 5;\n  // 顶导按钮信息\n  string top_button_label = 6;\n  // 城市ID\n  int32 city_id = 7;\n  // 城市名\n  string city_name = 8;\n}\n\n// 动态同城页-请求\nmessage DynOurCityReq {\n  // 城市ID\n  int64 city_id = 1;\n  // 纬度\n  double lat = 2;\n  // 经度\n  double lng = 3;\n  // 透传上一次接口请求返回的offset\n  string offset = 4;\n  // 每页元素个数\n  int32 page_size = 5;\n  // 青少年模式\n  // 1:开启青少年模式\n  int32 teenagers_mode = 6;\n  // 清晰度(旧版)\n  int32 qn = 7;\n  // 流版本(旧版)\n  int32 fnver = 8;\n  // 流类型(旧版)\n  int32 fnval = 9;\n  // 是否强制使用域名(旧版)\n  int32 force_host = 10;\n  // 是否4k(旧版)\n  int32 fourk = 11;\n  // 是否开启lbs\n  // 0:关闭 1:开启\n  int32 lbs_state = 12;\n  // 是否刷新城市\n  uint32 refresh_city = 13;\n  // 魔镜设置\n  ExpConf exp_conf = 14;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 15;\n  // 城市码\n  int64 city_code = 16;\n  // 构建时间\n  int64 build_time = 17;\n}\n\n// 动态同城开关-请求\nmessage DynOurCitySwitchReq {\n  // 开关参数\n  // 0:关闭 1:开启\n  int32 switch = 1;\n}\n\n// 红点接口物料\nmessage DynRedItem {\n  // 数字红点有效 更新数\n  uint64 count = 1;\n}\n\n// 红点接口-响应\nmessage DynRedReply {\n  // 类型\n  // count:数字红点 point:普通红点 no_point:没有红点\n  string red_type = 1;\n  // 红点具体信息\n  DynRedItem dyn_red_item = 2;\n  // 默认tab 值对应tab接口下发的anchor\n  string default_tab = 3;\n  //\n  DynRedStyle red_style = 4;\n}\n\n// 动态红点接口-请求\nmessage DynRedReq {\n  // 动态红点接口各tab offset信息\n  repeated TabOffset tab_offset = 1;\n}\n\n//\nmessage DynRedStyle {\n  //\n  int32 bg_type = 1;\n  //\n  int32 corner_type = 2;\n  //\n  int32 display_time = 3;\n  //\n  string corner_mark = 4;\n  //\n  DynRedStyleUp up = 5;\n  //\n  int32 type = 6;\n}\n\n//\nmessage DynRedStyleUp {\n  //\n  int64 uid = 1;\n  //\n  string face = 2;\n}\n\n// 动态tab详情\nmessage DynTab {\n  // tab标题 优先展示用,未开启状态第一次请求返回同城,后续请求返回对应城市名\n  string title = 1;\n  // 跳转链接\n  string uri = 2;\n  // 气泡内容\n  string bubble = 3;\n  // 是否推红点\n  int32 red_point = 4;\n  // 城市ID\n  int64 city_id = 5;\n  // 是否弹窗\n  int32 is_popup = 6;\n  // 弹窗内容\n  Popup popup = 7;\n  // 是否默认tab\n  bool defaultTab = 8;\n  // 副标题 对应城市名\n  string sub_title = 9;\n  // 锚点字段\n  string anchor = 10;\n  // 内测文案\n  string internal_test = 11;\n}\n\n// 动态tab页-响应\nmessage DynTabReply {\n  // 动态tab详情列表\n  repeated DynTab dyn_tab = 1;\n}\n\n// 动态tab页-请求\nmessage DynTabReq {\n  // 青少年模式\n  // 1:开启青少年模式\n  int32 teenagers_mode = 1;\n}\n\n// 最近访问-标记已读-请求\nmessage DynUpdOffsetReq {\n  // 被访问者的UID\n  int64 host_uid = 1;\n  // 用户已读进度\n  string read_offset = 2;\n}\n\n// 最近访问-个人feed流列表-响应\nmessage DynVideoPersonalReply {\n  // 动态列表\n  repeated DynamicItem list = 1;\n  // 偏移量\n  string offset = 2;\n  // 是否还有更多数据\n  int32 has_more = 3;\n  // 已读进度\n  string read_offset = 4;\n}\n\n// 最近访问-个人feed流列表-请求\nmessage DynVideoPersonalReq {\n  // 青少年模式\n  // 1:开启青少年模式\n  int32 teenagers_mode = 1;\n  // 被访问者的mid\n  int64 host_uid = 2;\n  // 偏移量 第一页可传空\n  string offset = 3;\n  // 标明下拉几次\n  int32 page = 4;\n  // 是否是预加载\n  int32 is_preload = 5;\n  // 清晰度\n  int32 qn = 6;\n  // 流版本\n  int32 fnver = 7;\n  // 流类型\n  int32 fnval = 8;\n  // 是否强制使用域名\n  int32 force_host = 9;\n  // 是否4k\n  int32 fourk = 10;\n}\n\n// 动态视频页-请求\nmessage DynVideoReq {\n  // 青少年模式\n  int32 teenagers_mode = 1;\n  // 透传 update_baseline\n  string update_baseline = 2;\n  // 透传 history_offset\n  string offset = 3;\n  // 向下翻页数\n  int32 page = 4;\n  // 刷新方式\n  // 1:向上刷新 2:向下翻页\n  int32 refresh_type = 5;\n  // 清晰度\n  int32 qn = 6;\n  // 流版本\n  int32 fnver = 7;\n  // 流类型\n  int32 fnval = 8;\n  // 是否强制使用域名\n  int32 force_host = 9;\n  // 是否4K\n  int32 fourk = 10;\n}\n\n// 动态视频页-响应\nmessage DynVideoReqReply {\n  // 动态列表\n  repeated DynamicItem list = 1;\n  // 更新的动态数\n  int32 update_num = 2;\n  // 历史偏移\n  string history_offset = 3;\n  // 更新基础信息\n  string update_baseline = 4;\n  // 是否还有更多数据\n  int32 has_more = 5;\n}\n\n// 魔镜实验配置项\nmessage Exp {\n  // 实验名\n  string exp_name = 1;\n  // 实验组\n  string exp_group = 2;\n}\n\n// 魔镜设置\nmessage ExpConf {\n  // 是否是魔镜请求\n  int32 exp_enable = 1;\n  // 实验配置\n  repeated Exp exps = 2;\n}\n\n// 拓展\nmessage Extend {\n  // 类型\n  // topic:话题小卡 lbs:lbs hot:热门视频 game:游戏\n  string type = 1;\n  // 卡片详情\n  oneof extend {\n    // 话题小卡\n    ExtInfoTopic ext_info_topic = 2;\n    // lbs\n    ExtInfoLBS ext_info_lbs = 3;\n    // 热门视频\n    ExtInfoHot ext_info_hot = 4;\n    // 游戏\n    ExtInfoGame ext_info_game = 5;\n  }\n}\n\n// 拓展信息-游戏小卡\nmessage ExtInfoGame {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n}\n\n// 拓展信息-热门视频\nmessage ExtInfoHot {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n}\n\n// 拓展信息-lbs\nmessage ExtInfoLBS {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n  // poiType\n  int32 poi_type = 4;\n}\n\n// 拓展信息-话题小卡\nmessage ExtInfoTopic {\n  // 标题-话题名\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n}\n\n// 折叠分类\nenum FoldType {\n  FoldTypeZero = 0; // 占位\n  FoldTypePublish = 1; // 用户发布折叠\n  FoldTypeFrequent = 2; // 转发超频折叠\n  FoldTypeUnite = 3; // 联合投稿折叠\n  FoldTypeLimit = 4; // 动态受限折叠\n}\n\n// 我的追番列表Item\nmessage FollowListItem {\n  // season_id\n  int32 season_id = 1;\n  // 标题\n  string title = 2;\n  // 封面图\n  string cover = 3;\n  // 跳转链接\n  string url = 4;\n  // 最新ep\n  NewEP new_ep = 5;\n}\n\n// 位置定位-响应\nmessage GeoCoderReply {\n  // 以行政区划+道路+门牌号等信息组成的标准格式化地址\n  string address = 1;\n  // 地址部件，address不满足需求时可自行拼接\n  AddressComponent address_component = 2;\n  // 行政区划信息\n  AdInfo ad_info = 3;\n}\n\n// 位置定位-请求\nmessage GeoCoderReq {\n  // 纬度\n  double lat = 1;\n  // 经度\n  double lng = 2;\n  // 页面来源\n  string from = 3;\n}\n\n// 行政区划中心点坐标\nmessage Gps {\n  // 纬度\n  double lat = 1;\n  // 经度\n  double lng = 2;\n}\n\n// 点赞动画\nmessage LikeAnimation {\n  // 开始动画\n  string begin = 1;\n  // 过程动画\n  string proc = 2;\n  // 结束动画\n  string end = 3;\n  // id\n  int64 like_icon_id = 4;\n}\n\n// 点赞拓展信息\nmessage LikeInfo {\n  // 点赞动画\n  LikeAnimation animation = 1;\n  // 是否点赞\n  int32 is_like = 2;\n}\n\n// 点赞用户\nmessage LikeUser {\n  // 用户mid\n  int64 uid = 1;\n  // 用户昵称\n  string uname = 2;\n  // 点击跳转链接\n  string uri = 3;\n}\n\n// 直播信息\nmessage LiveInfo {\n  // 是否在直播\n  // 0:未直播 1:正在直播\n  int32 is_living = 1;\n  // 跳转链接\n  string uri = 2;\n}\n\n// 播放器类型\nenum MediaType {\n  MediaTypeNone = 0; // 本地\n  MediaTypeUGC = 1; // UGC\n  MediaTypePGC = 2; // PGC\n  MediaTypeLive = 3; // 直播\n  MediaTypeVCS = 4; // 小视频\n}\n\n// 查看更多-列表单条数据\nmessage MixUpListItem {\n  // 用户mid\n  int64 uid = 1;\n  // 特别关注\n  // 0:否 1:是\n  int32 special_attention = 2;\n  // 小红点状态\n  // 0:没有 1:有\n  int32 reddot_state = 3;\n  // 直播信息\n  MixUpListLiveItem live_info = 4;\n  // 昵称\n  string name = 5;\n  // 头像\n  string face = 6;\n  // 认证信息\n  OfficialVerify official = 7;\n  // 大会员信息\n  VipInfo vip = 8;\n  // 关注状态\n  Relation relation = 9;\n  //\n  int32 premiere_state = 10;\n  //\n  string uri = 11;\n}\n\n// 直播信息\nmessage MixUpListLiveItem {\n  // 直播状态\n  // 0:未直播 1:直播中\n  bool status = 1;\n  // 房间号\n  int64 room_id = 2;\n  // 跳转地址\n  string uri = 3;\n}\n\n// 模块\nmessage Module {\n  // 类型\n  // fold:折叠 author:发布人 dynamic:动态卡片内容 state:计数信息 forward:转发 extend:小卡信息 dispute:争议小黄条 desc:描述信息\n  // likeUser:点赞用户 upList:最近访问列表 followList:我的追番\n  string module_type = 1;\n  oneof module_item{\n    // 折叠\n    ModuleFold module_fold = 2;\n    // 发布人\n    ModuleAuthor module_author = 3;\n    // 动态卡片内容\n    ModuleDynamic module_dynamic = 4;\n    // 计数信息\n    ModuleState module_state = 5;\n    // 转发\n    ModuleForward module_forward = 6;\n    // 小卡信息\n    ModuleExtend module_extend = 7;\n    // 争议小黄条\n    ModuleDispute module_dispute = 8;\n    // 描述信息\n    ModuleDesc module_desc = 9;\n    // 点赞用户\n    ModuleLikeUser module_likeUser = 10;\n    // 最近访问列表\n    ModuleDynUpList module_upList = 11;\n    // 我的追番\n    ModuleFollowList module_followList = 12;\n  }\n}\n\n// 作者信息模块\nmessage ModuleAuthor {\n  // 用户mid\n  int64 id = 1;\n  // 时间标签\n  string ptime_label_text = 2;\n  // 用户详情\n  UserInfo author = 3;\n  // 装扮卡片\n  DecorateCard decorate_card = 4;\n}\n\n// 文本内容模块\nmessage ModuleDesc {\n  // 文本描述\n  repeated Description desc = 1;\n}\n\n// 争议小黄条模块\nmessage ModuleDispute {\n  // 标题\n  string title = 1;\n  // 描述内容\n  string desc = 2;\n  // 跳转链接\n  string uri = 3;\n}\n\n// 动态详情模块\nmessage ModuleDynamic {\n  // 卡片类型\n  // ugc:ugc卡 pgc:pgc卡 currSeason:付费课程系列 currBatch:付费课程批次\n  string card_type = 1;\n  // 正文卡片\n  oneof card {\n    // ugc卡\n    CardUGC card_ugc = 2;\n    // pgc卡\n    CardPGC card_pgc = 3;\n    // 付费课程系列\n    CardCurrSeason card_curr_season = 4;\n    // 付费课程批次\n    CardCurrBatch card_curr_batch = 5;\n  }\n}\n\n// 最近访问up主列表\nmessage ModuleDynUpList {\n  // 标题展示文案\n  string module_title = 1;\n  // “全部”按钮文案\n  string show_all = 2;\n  // up主列表\n  repeated UpListItem list = 3;\n}\n\n// 拓展信息\nmessage ModuleExtend {\n  // 拓展\n  repeated Extend extend = 1;\n}\n\n// 折叠模块\nmessage ModuleFold {\n  // 折叠分类(该字段废弃)\n  int32 fold_type = 1;\n  // 折叠文案\n  string text = 2;\n  // 被折叠的动态\n  string fold_ids = 3;\n  // 被折叠的用户信息\n  repeated UserInfo fold_users = 4;\n  // 折叠分类\n  FoldType fold_type_v2 = 5;\n}\n\n// 我的追番列表\nmessage ModuleFollowList {\n  // 查看全部的跳转链接\n  string view_all_link = 1;\n  //\n  repeated FollowListItem list = 2;\n}\n\n// 转发模块\nmessage ModuleForward {\n  // 卡片类型\n  string card_type = 1;\n  // 嵌套模型\n  repeated Module modules = 2;\n}\n\n// 点赞用户模块\nmessage ModuleLikeUser {\n  // 点赞用户\n  repeated LikeUser like_users = 1;\n  // 文案\n  string display_text = 2;\n}\n\n// 计数信息模块\nmessage ModuleState {\n  // 转发数\n  int32 repost = 1;\n  // 点赞数\n  int32 like = 2;\n  // 评论数\n  int32 reply = 3;\n  // 点赞拓展信息\n  LikeInfo like_info = 4;\n  // 禁评\n  bool no_comment = 5;\n  // 禁转\n  bool no_forward = 6;\n}\n\n// 认证名牌\nmessage Nameplate {\n  // nid\n  int64 nid = 1;\n  // 名称\n  string name = 2;\n  // 图片地址\n  string image = 3;\n  // 小图地址\n  string image_small = 4;\n  // 等级\n  string level = 5;\n  // 获取条件\n  string condition = 6;\n}\n\n// 最新ep\nmessage NewEP {\n  // 最新话epid\n  int32 id = 1;\n  // 更新至XX话\n  string index_show = 2;\n  // 更新剧集的封面\n  string cover = 3;\n}\n\n// 空响应\nmessage NoReply {\n\n}\n\n// 空请求\nmessage NoReq {\n\n}\n\n// 认证信息\nmessage OfficialVerify {\n  // 认证类型\n  // 127:未认证 0:个人 1:机构\n  int32 type = 1;\n  // 认证描述\n  string desc = 2;\n  //\n  int32 is_atten = 3;\n}\n\n// 动态同城点击上报-响应\nmessage OurCityClickReportReply {\n\n}\n\n// 动态同城点击上报-请求\nmessage OurCityClickReportReq {\n  // 动态ID\n  string dynamic_id = 1;\n  // 城市ID\n  int64 city_id = 2;\n  // 纬度\n  double lat = 3;\n  // 经度\n  double lng = 4;\n}\n\n// PGC单季信息\nmessage PGCSeason {\n  // 是否完结\n  int32 is_finish = 1;\n  // 标题\n  string title = 2;\n  // 类型\n  int32 type = 3;\n}\n\n// 秒开参数\nmessage PlayerPreloadParams {\n  // 清晰度\n  int32 qn = 1;\n  // 流版本\n  int32 fnver = 2;\n  // 流类型\n  int32 fnval = 3;\n  // 是否强制使用域名\n  int32 force_host = 4;\n  // 是否4k\n  int32 fourk = 5;\n}\n\n// 动态tab弹窗详情\nmessage Popup {\n  // 标题\n  string title = 1;\n  // 文案\n  string desc = 2;\n  // 文案附加跳转地址\n  string uri = 3;\n}\n\n// 关注关系\nmessage Relation {\n  // 关注状态\n  RelationStatus status = 1;\n  // 关注\n  int32 is_follow = 2;\n  // 被关注\n  int32 is_followed = 3;\n  // 文案\n  string title = 4;\n}\n\n// 关注状态\nenum RelationStatus {\n  relation_status_none = 0; //\n  relation_status_nofollow = 1; // 未关注\n  relation_status_follow = 2; // 关注\n  relation_status_followed = 3; // 被关注\n  relation_status_mutual_concern = 4; // 互相关注\n  relation_status_special = 5; // 特别关注\n}\n\n// 分享需要\nmessage ShareInfo {\n  // 稿件avid\n  int64 aid = 1;\n  // 稿件bvid\n  string bvid = 2;\n  // 标题\n  string title = 3;\n  // 副标题\n  string subtitle = 4;\n  // 稿件封面\n  string cover = 5;\n  // up mid\n  int64 mid = 6;\n  // up昵称\n  string name = 7;\n}\n\n//\nenum StyleType {\n  STYLE_TYPE_NONE = 0; //\n  STYLE_TYPE_LIVE = 1; //\n  STYLE_TYPE_DYN_UP = 2; //\n}\n\n// 小视频卡片项\nmessage SVideoItem {\n  // 卡片类型\n  // av:稿件视频\n  string card_type = 1;\n  // 模块内容\n  repeated SVideoModule modules = 2;\n  // 动态ID str\n  string dyn_id_str = 3;\n  // 卡片游标\n  int64 index = 4;\n}\n\n// 小视频模块\nmessage SVideoModule {\n  // 类型\n  // author:发布人 player:播放器内容 desc:描述信息 stat:计数信息\n  string module_type = 1;\n  oneof module_item {\n    // 发布人\n    SVideoModuleAuthor module_author = 2;\n    // 播放器内容\n    SVideoModulePlayer module_player = 3;\n    // 描述信息\n    SVideoModuleDesc module_desc = 4;\n    // 计数信息\n    SVideoModuleStat module_stat = 5;\n  }\n}\n\n// 作者信息模块\nmessage SVideoModuleAuthor {\n  // 用户mid\n  int64 mid = 1;\n  // 用户昵称\n  string name = 2;\n  // 用户头像\n  string face = 3;\n  // 发布描述\n  string pub_desc = 4;\n  // 是否关注up\n  // 1:已关注\n  int32 is_attention = 5;\n  // 跳转地址\n  string uri = 6;\n}\n\n// 文本内容模块\nmessage SVideoModuleDesc {\n  // 文本内容\n  string text = 1;\n  // 跳转地址\n  string uri = 2;\n}\n\n// 播放器模块\nmessage SVideoModulePlayer {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 跳转地址，秒开地址如果有会拼接player_preload可参考天马\n  string uri = 3;\n  // aid\n  int64 aid = 4;\n  // cid\n  int64 cid = 5;\n  // 视频时长\n  int64 duration = 6;\n  // 尺寸信息\n  Dimension dimension = 7;\n}\n\n// 计数信息模块\nmessage SVideoModuleStat {\n  // 计数内容\n  repeated SVideoStatInfo stat_info = 1;\n  // 分享需要\n  ShareInfo share_info = 2;\n}\n\n// 小视频连播页-响应\nmessage SVideoReply {\n  // 列表\n  repeated SVideoItem list = 1;\n  // 翻页游标\n  string offset = 2;\n  // 是否还有更多数据\n  // 1:有\n  int32 has_more = 3;\n  // 顶部\n  SVideoTop top = 4;\n}\n\n// 小视频连播页-请求\nmessage SVideoReq {\n  // 当前素材的id\n  int64 oid = 1;\n  // 当前素材类型\n  // 1:动态(如果有oid则必传) 2:热门分类 3:热点聚合\n  SVideoType type = 2;\n  // 翻页offset\n  string offset = 3;\n  // 清晰度(旧版)\n  int32 qn = 4;\n  // 流版本(旧版)\n  int32 fnver = 5;\n  // 流类型(旧版)\n  int32 fnval = 6;\n  // 是否强制使用域名(旧版)\n  int32 force_host = 7;\n  // 是否4k(旧版)\n  int32 fourk = 8;\n  // 当前页面spm\n  string spmid = 9;\n  // 上级页面spm\n  string from_spmid = 10;\n  // 秒开参数\n  PlayerPreloadParams player_preload = 11;\n  // 热门进入联播页的锚点aid\n  int64 focus_aid = 12;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 13;\n}\n\n// 计数内容\nmessage SVideoStatInfo {\n  // 计数icon\n  // 1:分享符号 2:评论符号 3:点赞符号\n  int32 icon = 1;\n  // 计数值\n  int64 num = 2;\n  // 选中状态\n  // 1:选中\n  int32 selected = 3;\n  // 跳转链接(如评论)\n  string uri = 4;\n}\n\n// 顶部\nmessage SVideoTop {\n  // 联播页标题\n  string Title = 1;\n  // 联播页导语\n  string Desc = 2;\n}\n\n// 入口联播页类型\nenum SVideoType {\n  TypeNone = 0; // 无类型\n  TypeDynamic = 1; // 动态\n  TypePopularIndex = 2; // 热门分类\n  TypePopularHotword = 3; // 热点聚合\n}\n\n// 动态红点接口各tab offset信息\nmessage TabOffset {\n  // 1:综合页 2:视频页\n  int32  tab = 1;\n  // 上一次对应列表页offset\n  string offset = 2;\n}\n\n// up主列表\nmessage UpListItem {\n  // 是否有更新\n  // 0:没有 1:有\n  int32 has_update = 1;\n  // up主头像\n  string face = 2;\n  // up主昵称\n  string name = 3;\n  // up主uid\n  int64 uid = 4;\n}\n\n// 用户信息\nmessage UserInfo {\n  // 用户mid\n  int64 mid = 1;\n  // 用户昵称\n  string name = 2;\n  // 用户头像\n  string face = 3;\n  // 认证信息\n  OfficialVerify official = 4;\n  // 大会员信息\n  VipInfo vip = 5;\n  // 直播信息\n  LiveInfo live = 6;\n  // 空间页跳转链接\n  string uri = 7;\n  // 挂件信息\n  UserPendant pendant = 8;\n  // 认证名牌\n  Nameplate nameplate = 9;\n}\n\n// 头像挂件信息\nmessage UserPendant {\n  // pid\n  int64 pid = 1;\n  // 名称\n  string name = 2;\n  // 图片链接\n  string image = 3;\n  // 有效期\n  int64 expire = 4;\n}\n\n// 角标信息\nmessage VideoBadge {\n  // 文案\n  string text = 1;\n  // 文案颜色-日间\n  string text_color = 2;\n  // 文案颜色-夜间\n  string text_color_night = 3;\n  // 背景颜色-日间\n  string bg_color = 4;\n  // 背景颜色-夜间\n  string bg_color_night = 5;\n  // 边框颜色-日间\n  string border_color = 6;\n  // 边框颜色-夜间\n  string border_color_night = 7;\n  // 样式\n  int32 bg_style = 8;\n}\n\n// 番剧类型\nenum VideoSubType {\n  VideoSubTypeNone = 0; // 没有子类型\n  VideoSubTypeBangumi = 1; // 番剧\n  VideoSubTypeMovie = 2; // 电影\n  VideoSubTypeDocumentary = 3; // 纪录片\n  VideoSubTypeDomestic = 4; // 国创\n  VideoSubTypeTeleplay = 5; // 电视剧\n}\n\n// 大会员信息\nmessage VipInfo {\n  // 大会员类型\n  int32 Type = 1;\n  // 大会员状态\n  int32 status = 2;\n  // 到期时间\n  int64 due_date = 3;\n  // 标签\n  VipLabel label = 4;\n  // 主题\n  int32 theme_type = 5;\n}\n\n// 大会员标签\nmessage VipLabel {\n  // 图片地址\n  string path = 1;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/dynamic/v2/campus.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.dynamic.v2;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\nimport \"bilibili/pagination/pagination.proto\";\nimport \"bilibili/app/dynamic/common/dynamic.proto\";\nimport \"bilibili/app/dynamic/v2/dynamic.proto\";\n\nservice Campus {\n  //\n  //rpc WaterFlowRcmd (WaterFlowRcmdReq) returns (WaterFlowRcmdResp);\n}\n\n//\nmessage CampusWaterFlowItem {\n  //\n  int32 item_type = 1;\n  //\n  bilibili.app.dynamic.common.ItemWHRatio wh_ratio = 2;\n  //\n  oneof item {\n    WFItemDefault item_default = 3;\n  }\n}\n\n//\nmessage WaterFlowRcmdReq {\n  //\n  int64 campus_id = 1;\n  //\n  int32 page = 2;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 3;\n  //\n  //CampusRcmdReqFrom from = 4;\n}\n\n//\nmessage WaterFlowRcmdResp {\n  //\n  repeated CampusWaterFlowItem items = 1;\n  //\n  bilibili.pagination.FeedPaginationReply offset = 2;\n}\n\n//\nmessage WFItemDefault {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  //CoverIconWithText bottom_left_1 = 3;\n  //\n  //CoverIconWithText bottom_left_2 = 4;\n  //\n  //CoverIconWithText bottom_right_1 = 5;\n  //\n  string uri = 6;\n  //\n  //RcmdReason rcmd_reason = 7;\n  //\n  map<string, string> annotations = 8;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/dynamic/v2/dynamic.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.dynamic.v2;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/any.proto\";\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\nimport \"bilibili/dagw/component/avatar/v1/avatar.proto\";\n\n// v2动态, rpc 按字母顺序排列\nservice Dynamic {\n  //\n  rpc AlumniDynamics (AlumniDynamicsReq) returns (AlumniDynamicsReply);\n  //\n  rpc CampusBillBoard (CampusBillBoardReq) returns (CampusBillBoardReply);\n  //\n  rpc CampusEntryTab(CampusEntryTabReq) returns (CampusEntryTabResp);\n  //\n  rpc CampusFeedback(CampusFeedbackReq) returns (CampusFeedbackReply);\n  //\n  rpc CampusHomePages(CampusHomePagesReq) returns (CampusHomePagesReply);\n  //\n  rpc CampusMateLikeList(CampusMateLikeListReq) returns (CampusMateLikeListReply);\n  //\n  rpc CampusMngDetail(CampusMngDetailReq) returns (CampusMngDetailReply);\n  //\n  rpc CampusMngQuizOperate(CampusMngQuizOperateReq) returns (CampusMngQuizOperateReply);\n  //\n  rpc CampusMngSubmit(CampusMngSubmitReq) returns (CampusMngSubmitReply);\n  //\n  rpc CampusRcmd(CampusRcmdReq) returns (CampusRcmdReply);\n  //\n  rpc CampusRcmdFeed(CampusRcmdFeedReq) returns (CampusRcmdFeedReply);\n  //\n  rpc CampusRecommend(CampusRecommendReq) returns (CampusRecommendReply);\n  //\n  rpc CampusRedDot(CampusRedDotReq) returns (CampusRedDotReply);\n  //\n  rpc CampusSquare(CampusSquareReq) returns (CampusSquareReply);\n  //\n  rpc CampusTopicRcmdFeed(CampusTopicRcmdFeedReq) returns (CampusTopicRcmdFeedReply);\n  // 动态通用附加卡-follow/取消follow\n  rpc DynAdditionCommonFollow(DynAdditionCommonFollowReq) returns (DynAdditionCommonFollowReply);\n  // 动态综合页\n  rpc DynAll(DynAllReq) returns (DynAllReply);\n  // 综合页最近访问 - 个人feed流\n  rpc DynAllPersonal(DynAllPersonalReq) returns (DynAllPersonalReply);\n  // 综合页最近访问 - 标记已读\n  rpc DynAllUpdOffset(DynAllUpdOffsetReq) returns (NoReply);\n  // 动态详情页\n  rpc DynDetail(DynDetailReq) returns (DynDetailReply);\n  // 批量动态id获取动态详情\n  rpc DynDetails(DynDetailsReq) returns (DynDetailsReply);\n  // 动态发布生成临时卡\n  rpc DynFakeCard(DynFakeCardReq) returns (DynFakeCardReply);\n  //\n  rpc DynFriend(DynFriendReq) returns (DynFriendReply);\n  // 轻浏览\n  rpc DynLight(DynLightReq) returns (DynLightReply);\n  // 网关调用 - 查看更多-列表\n  rpc DynMixUpListViewMore(DynMixUpListViewMoreReq) returns (DynMixUpListViewMoreReply);\n  // 关注推荐up主换一换\n  rpc DynRcmdUpExchange(DynRcmdUpExchangeReq) returns (DynRcmdUpExchangeReply);\n  //\n  rpc DynSearch(DynSearchReq) returns (DynSearchReply);\n  //\n  rpc DynServerDetails(DynServerDetailsReq) returns (DynServerDetailsReply);\n  // 空间页动态\n  rpc DynSpace(DynSpaceReq) returns (DynSpaceRsp);\n  //\n  rpc DynSpaceSearchDetails(DynSpaceSearchDetailsReq) returns (DynSpaceSearchDetailsReply);\n  //\n  rpc DynTab(DynTabReq) returns (DynTabReply);\n  // 动态点赞\n  rpc DynThumb(DynThumbReq) returns (NoReply);\n  // 未登录页分区UP主推荐\n  rpc DynUnLoginRcmd(DynRcmdReq) returns (DynRcmdReply);\n  // 动态视频页\n  rpc DynVideo(DynVideoReq) returns (DynVideoReply);\n  // 视频页最近访问 - 个人feed流\n  rpc DynVideoPersonal(DynVideoPersonalReq) returns (DynVideoPersonalReply);\n  // 视频页最近访问 - 标记已读\n  rpc DynVideoUpdOffset(DynVideoUpdOffsetReq) returns (NoReply);\n  //\n  rpc DynVote(DynVoteReq) returns (DynVoteReply);\n  //\n  rpc FeedFilter(FeedFilterReq) returns (FeedFilterReply);\n  //\n  rpc FetchTabSetting(NoReq) returns (FetchTabSettingReply);\n  //\n  rpc HomeSubscribe(HomeSubscribeReq) returns (HomeSubscribeReply);\n  //\n  rpc LbsPoi(LbsPoiReq) returns (LbsPoiReply);\n  //\n  rpc LegacyTopicFeed(LegacyTopicFeedReq) returns (LegacyTopicFeedReply);\n  // 点赞列表\n  rpc LikeList(LikeListReq) returns (LikeListReply);\n  //\n  rpc OfficialAccounts(OfficialAccountsReq) returns (OfficialAccountsReply);\n  //\n  rpc OfficialDynamics(OfficialDynamicsReq) returns (OfficialDynamicsReply);\n  // 新版动态转发点赞列表 需要登录\n  rpc ReactionList(ReactionListReq) returns (ReactionListReply);\n  // 转发列表\n  rpc RepostList(RepostListReq) returns (RepostListRsp);\n  //\n  rpc SchoolRecommend(SchoolRecommendReq) returns (SchoolRecommendReply);\n  //\n  rpc SchoolSearch(SchoolSearchReq) returns (SchoolSearchReply);\n  //\n  rpc SetDecision(SetDecisionReq) returns (NoReply);\n  //\n  rpc SetRecentCampus(SetRecentCampusReq) returns (NoReply);\n  //\n  rpc SubscribeCampus(SubscribeCampusReq) returns (NoReply);\n  //\n  rpc TopicList(TopicListReq) returns (TopicListReply);\n  //\n  rpc TopicSquare(TopicSquareReq) returns (TopicSquareReply);\n  //\n  rpc UnfollowMatch(UnfollowMatchReq) returns (NoReply);\n  //\n  rpc UpdateTabSetting(UpdateTabSettingReq) returns (NoReply);\n}\n\n//\nenum AddButtonBgStyle {\n  fill = 0;   // 默认填充\n  stroke = 1; // 描边\n  gray = 2;   // 置灰\n}\n\n// 按钮类型\nenum AddButtonType {\n  bt_none = 0;   // 占位\n  bt_jump = 1;   // 跳转\n  bt_button = 2; // 按钮\n}\n\n// 活动皮肤\nmessage AdditionalActSkin {\n  // 动画SVGA资源\n  string svga = 1;\n  // 动画SVGA最后一帧图片资源\n  string last_image = 2;\n  // 动画播放次数\n  int64 play_times = 3;\n}\n\n// 动态-附加卡-按钮\nmessage AdditionalButton {\n  // 按钮类型\n  AddButtonType type = 1;\n  // jump-跳转样式\n  AdditionalButtonStyle jump_style = 2;\n  // jump-跳转链接\n  string jump_url = 3;\n  // button-未点样式\n  AdditionalButtonStyle uncheck = 4;\n  // button-已点样式\n  AdditionalButtonStyle check = 5;\n  // button-当前状态\n  AdditionalButtonStatus status = 6;\n  // 按钮点击样式\n  AdditionalButtonClickType click_type = 7;\n}\n\n// 附加卡按钮点击类型\nenum AdditionalButtonClickType {\n  click_none = 0; // 通用按钮\n  click_up = 1;   // 预约卡按钮\n}\n\n//\nmessage AdditionalButtonInteractive {\n  // 是否弹窗\n  string popups = 1;\n  // 弹窗确认文案\n  string confirm = 2;\n  // 弹窗取消文案\n  string cancel = 3;\n  //\n  string desc = 4;\n}\n\n//\nmessage AdditionalButtonShare {\n  //\n  int32 show = 1;\n  //\n  string icon = 2;\n  //\n  string text = 3;\n}\n\n// 附加卡按钮状态\nenum AdditionalButtonStatus {\n  none = 0;    //\n  uncheck = 1; //\n  check = 2;   //\n}\n\n// 动态-附加卡-按钮样式\nmessage AdditionalButtonStyle {\n  // icon\n  string icon = 1;\n  // 文案\n  string text = 2;\n  // 按钮点击交互\n  AdditionalButtonInteractive interactive = 3;\n  // 当前按钮填充样式\n  AddButtonBgStyle bg_style = 4;\n  // toast文案, 当disable=1时有效\n  string toast = 5;\n  // 当前按钮样式,\n  // 0:高亮 1:置灰(按钮不可点击)\n  DisableState disable = 6;\n  //\n  AdditionalButtonShare share = 7;\n}\n\n// 动态-附加卡-番剧卡\nmessage AdditionalPGC {\n  // 头部说明文案\n  string head_text = 1;\n  // 标题\n  string title = 2;\n  // 展示图\n  string image_url = 3;\n  // 描述文字1\n  string desc_text_1 = 4;\n  // 描述文字2\n  string desc_text_2 = 5;\n  // 点击跳转链接\n  string url = 6;\n  // 按钮\n  AdditionalButton button = 7;\n  // 头部icon\n  string head_icon = 8;\n  // style\n  ImageStyle style = 9;\n  // 动态本身的类型 type\n  string type = 10;\n}\n\n//\nenum AdditionalShareShowType {\n  st_none = 0; //\n  st_show = 1; //\n}\n\n// 枚举-动态附加卡\nenum AdditionalType {\n  additional_none = 0;                // 占位\n  additional_type_pgc = 1;            // 附加卡-追番\n  additional_type_goods = 2;          // 附加卡-商品\n  additional_type_vote = 3;           // 附加卡投票\n  additional_type_common = 4;         // 附加通用卡\n  additional_type_esport = 5;         // 附加电竞卡\n  additional_type_up_rcmd = 6;        // 附加UP主推荐卡\n  additional_type_ugc = 7;            // 附加卡-ugc\n  additional_type_up_reservation = 8; // UP主预约卡\n}\n\n// 动态-附加卡-专栏\nmessage AdditionArticle {\n  //\n  string title = 1;\n  //\n  MdlDynDrawItem cover = 2;\n  //\n  string desc_text_left = 3;\n  //\n  string desc_text_right = 4;\n  //\n  string uri = 5;\n  //\n  string card_type = 6;\n}\n\n// 动态-附加卡-通用卡\nmessage AdditionCommon {\n  // 头部说明文案\n  string head_text = 1;\n  // 标题\n  string title = 2;\n  // 展示图\n  string image_url = 3;\n  // 描述文字1\n  string desc_text_1 = 4;\n  // 描述文字2\n  string desc_text_2 = 5;\n  // 点击跳转链接\n  string url = 6;\n  // 按钮\n  AdditionalButton button = 7;\n  // 头部icon\n  string head_icon = 8;\n  // style\n  ImageStyle style = 9;\n  // 动态本身的类型 type\n  string type = 10;\n  // 附加卡类型\n  string card_type = 11; // ogv manga\n}\n\n// 动态-附加卡-电竞卡\nmessage AdditionEsport {\n  // 电竞类型\n  EspaceStyle style = 1;\n  oneof item {\n    // moba类\n    AdditionEsportMoba addition_esport_moba = 2;\n  }\n  // 动态本身的类型 type\n  string type = 3;\n  // 附加卡类型\n  string card_type = 4; // ogv manga\n}\n\n// 动态-附加卡-电竞卡-moba类\nmessage AdditionEsportMoba {\n  // 头部说明文案\n  string head_text = 1;\n  // 标题\n  string title = 2;\n  // 战队列表\n  repeated MatchTeam match_team = 3;\n  // 比赛信息\n  AdditionEsportMobaStatus addition_esport_moba_status = 4;\n  // 卡片跳转\n  string uri = 5;\n  // 按钮\n  AdditionalButton button = 6;\n  // 副标题\n  string sub_title = 7;\n  // 动态本身的类型 type\n  string type = 10;\n  // 附加卡类型\n  string card_type = 11;\n  // 附加卡图标\n  string head_icon = 12;\n}\n\n// 动态-附加卡-电竞卡-moba类-比赛信息\nmessage AdditionEsportMobaStatus {\n  // 文案类\n  repeated AdditionEsportMobaStatusDesc addition_esport_moba_status_desc = 1;\n  // 比赛状态文案\n  string title = 2;\n  // 比赛状态状态\n  int32 status = 3;\n  // 日间色值\n  string color = 4;\n  // 夜间色值\n  string night_color = 5;\n}\n\n// 动态-附加卡-电竞卡-moba类-比赛信息-文案类\nmessage AdditionEsportMobaStatusDesc {\n  // 文案\n  string title = 1;\n  // 日间色值\n  string color = 2;\n  // 夜间色值\n  string night_color = 3;\n}\n\n// 动态-附加卡-商品卡\nmessage AdditionGoods {\n  // 推荐文案\n  string rcmd_desc = 1;\n  // 商品信息\n  repeated GoodsItem goods_items = 2;\n  // 附加卡类型\n  string card_type = 3;\n  // 头部icon\n  string icon = 4;\n  // 商品附加卡整卡跳转\n  string uri = 5;\n  // 商品类型\n  // 1:淘宝 2:会员购，注：实际是获取的goods_items里面的第一个source_type\n  int32 source_type = 6;\n  //\n  int32 jump_type = 7;\n  //\n  string app_name = 8;\n  //\n  string ad_mark_icon = 9;\n}\n\n// 动态-附加卡-直播附加卡\nmessage AdditionLiveRoom {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  VideoBadge badge = 3;\n  //\n  CoverIconWithText desc_text_upper = 4;\n  //\n  string desc_text_lower = 5;\n  //\n  string uri = 6;\n  //\n  string card_type = 7;\n}\n\n// 动态-附加卡-UGC视频附加卡\nmessage AdditionUgc {\n  // 说明文案\n  string head_text = 1;\n  // 稿件标题\n  string title = 2;\n  // 封面\n  string cover = 3;\n  // 描述文字1\n  string desc_text_1 = 4;\n  // 描述文字2\n  string desc_text_2 = 5;\n  // 接秒开\n  string uri = 6;\n  // 时长\n  string duration = 7;\n  // 标题支持换行-标题支持单行和双行，本期不支持填充up昵称，支持双行展示，字段默认为true\n  bool line_feed = 8;\n  // 附加卡类型\n  string card_type = 9;\n}\n\n// up主预约发布卡\nmessage AdditionUP {\n  // 标题\n  string title = 1;\n  // 高亮文本，描述文字1\n  HighlightText desc_text_1 = 2;\n  // 描述文字2\n  string desc_text_2 = 3;\n  // 点击跳转链接\n  string url = 4;\n  // 按钮\n  AdditionalButton button = 5;\n  // 附加卡类型\n  string card_type = 6;\n  // 预约人数(用于预约人数变化)\n  int64 reserve_total = 7;\n  // 活动皮肤\n  AdditionalActSkin act_skin = 8;\n  // 预约id\n  int64 rid = 9;\n  //\n  int32 lottery_type = 10;\n  //\n  HighlightText desc_text3 = 11;\n  //\n  int64 up_mid = 12;\n  //\n  AdditionUserInfo user_info = 13;\n  //\n  string dynamic_id = 14;\n  //\n  bool show_text2 = 15;\n  //\n  int64 dyn_type = 16;\n  //\n  string business_id = 17;\n  //\n  string badge_text = 18;\n  //\n  bool is_premiere = 19;\n}\n\n//\nmessage AdditionUserInfo {\n  //\n  string name = 1;\n  //\n  string face = 2;\n}\n\n// 动态-附加卡-投票\nmessage AdditionVote {\n  // 封面图\n  string image_url = 1;\n  // 标题\n  string title = 2;\n  // 展示项1\n  string text_1 = 3;\n  // button文案\n  string button_text = 4;\n  // 点击跳转链接\n  string url = 5;\n}\n\n// 动态模块-投票\nmessage AdditionVote2 {\n  // 投票类型\n  AdditionVoteType addition_vote_type = 1;\n  // 投票ID\n  int64 vote_id = 2;\n  // 标题\n  string title = 3;\n  // 已过期： xxx人参与· 投票已过期。button 展示去查看\n  // 未过期： xxx人参与· 剩xx天xx时xx分。button展示去投票\n  string label = 4;\n  // 剩余时间\n  int64 deadline = 5;\n  // 生效文案\n  string open_text = 6;\n  // 过期文案\n  string close_text = 7;\n  // 已投票\n  string voted_text = 8;\n  // 投票状态\n  AdditionVoteState state = 9;\n  // 投票信息\n  oneof item {\n    //\n    AdditionVoteWord addition_vote_word = 10;\n    //\n    AdditionVotePic addition_vote_pic = 11;\n    //\n    AdditionVoteDefaule addition_vote_defaule = 12;\n  }\n  // 业务类型\n  // 0:动态投票 1:话题h5组件\n  int32 biz_type = 13;\n  // 投票总人数\n  int64 total = 14;\n  // 附加卡类型\n  string card_type = 15;\n  // 异常提示\n  string tips = 16;\n  // 跳转地址\n  string uri = 17;\n  // 是否投票\n  bool is_voted = 18;\n  // 投票最多多选个数，单选为1\n  int32 choice_cnt = 19;\n  // 是否默认选中分享到动态\n  bool defaule_select_share = 20;\n}\n\n// 外露投票\nmessage AdditionVoteDefaule {\n  // 图片 多张\n  repeated string cover = 1;\n}\n\n// 外露图片类型\nmessage AdditionVotePic {\n  // 图片投票详情\n  repeated AdditionVotePicItem item = 1;\n}\n\n// 图片投票详情\nmessage AdditionVotePicItem {\n  // 选项索引，从1开始\n  int32 opt_idx = 1;\n  // 图片\n  string cover = 2;\n  // 选中状态\n  bool is_vote = 3;\n  // 人数\n  int32 total = 4;\n  // 占比\n  double persent = 5;\n  // 标题文案\n  string title = 6;\n  // 是否投票人数最多的选项\n  bool  is_max_option = 7;\n}\n\n// 投票状态\nenum AdditionVoteState {\n  addition_vote_state_none = 0;  //\n  addition_vote_state_open = 1;  //\n  addition_vote_state_close = 2; //\n}\n\n// 投票类型\nenum AdditionVoteType {\n  addition_vote_type_none = 0;    //\n  addition_vote_type_word = 1;    //\n  addition_vote_type_pic = 2;     //\n  addition_vote_type_default = 3; //\n}\n\n\n// 外露文字类型\nmessage AdditionVoteWord {\n  // 外露文字投票详情\n  repeated AdditionVoteWordItem item = 1;\n}\n\n// 外露文字投票详情\nmessage AdditionVoteWordItem {\n  // 选项索引，从1开始\n  int32 opt_idx = 1;\n  // 文案\n  string title = 2;\n  // 选中状态\n  bool is_vote = 3;\n  // 人数\n  int32 total = 4;\n  // 占比\n  double persent = 5;\n  // 是否投票人数最多的选项\n  bool  is_max_option = 6;\n}\n\n// 综合页请求广告所需字段，由客户端-网关透传\nmessage AdParam {\n  // 综合页请求广告所需字段，由客户端-网关透传\n  string ad_extra = 1;\n  // request_id\n  string request_id = 2;\n}\n\n//\nmessage AlumniDynamicsReply {\n  //\n  repeated DynamicItem list = 1;\n  //\n  string toast = 2;\n}\n\n//\nmessage AlumniDynamicsReq {\n  //\n  int64 campus_id = 1;\n  //\n  int32 first_time = 2;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 3;\n  //\n  int32 local_time = 4;\n  //\n  int32 page = 5;\n  //\n  int32 from_type = 6;\n}\n\n//\nmessage CampusBannerInfo {\n  //\n  string image = 1;\n  //\n  string jump_url = 2;\n}\n\n//\nmessage CampusBillboardInternalReq {\n  //\n  int64 mid = 1;\n  //\n  int64 campus_id = 2;\n  //\n  string version_code = 3;\n}\n\n//\nmessage CampusBillBoardReply {\n  //\n  string title = 1;\n  //\n  string help_uri = 2;\n  //\n  string campus_name = 3;\n  //\n  int64 build_time = 4;\n  //\n  string version_code = 5;\n  //\n  repeated OfficialItem list = 6;\n  //\n  string share_uri = 7;\n  //\n  int32 bind_notice = 8;\n  //\n  string update_toast = 9;\n  //\n  int64 campus_id = 10;\n  //\n  CampusFeatureProgress open_progress = 11;\n}\n\n//\nmessage CampusBillBoardReq {\n  //\n  int64 campus_id = 1;\n  //\n  string version_code = 2;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 3;\n  //\n  CampusReqFromType from_type = 4;\n}\n\n//\nmessage CampusEntryTabReq {\n  //\n  int64 campus_id = 1;\n}\n\n//\nmessage CampusEntryTabResp {\n  //\n  CampusEntryType entry_type = 1;\n}\n\n//\nenum CampusEntryType {\n  //\n  NONE = 0;\n  //\n  ENTRY_DYNAMIC = 1;\n  //\n  ENTRY_HOME = 2;\n}\n\n//\nmessage CampusFeatureProgress {\n  //\n  int64 progress_full = 1;\n  //\n  int64 progress_achieved = 2;\n  //\n  string desc_title = 3;\n  //\n  string desc_1 = 4;\n  //\n  CampusLabel btn = 5;\n}\n\n//\nmessage CampusFeedbackInfo {\n  //\n  int32 biz_type = 1;\n  //\n  int64 biz_id = 2;\n  //\n  int64 campus_id = 3;\n  //\n  string reason = 4;\n}\n\n//\nmessage CampusFeedbackReply {\n  //\n  string message = 1;\n}\n\n//\nmessage CampusFeedbackReq {\n  //\n  repeated CampusFeedbackInfo infos = 1;\n  //\n  int32 from = 2;\n}\n\n//\nmessage CampusHomePagesReply {\n  //\n  CampusRcmdTop top = 1;\n  //\n  CampusTop campus_top = 2;\n  //\n  int32 page_type = 3;\n}\n\n//\nmessage CampusHomePagesReq {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  double lat = 3;\n  //\n  double lng = 4;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 5;\n  //\n  int32 page_type = 6;\n}\n\nenum CampusRcmdReqFrom {\n  CAMPUS_RCMD_FROM_UNKNOWN = 0;\n  CAMPUS_RCMD_FROM_HOME_UN_OPEN = 1;\n  CAMPUS_RCMD_FROM_VISIT_OTHER = 2;\n  CAMPUS_RCMD_FROM_HOME_MOMENT = 3;\n  CAMPUS_RCMD_FROM_DYN_MOMENT = 4;\n  CAMPUS_RCMD_FROM_PAGE_SUBORDINATE_MOMENT = 5;\n}\n\n//\nenum CampusHomePageType {\n  //\n  PAGE_MAJOR = 0;\n  //\n  PAGE_SUBORDINATE = 1;\n  //\n  PAGE_MAJOR_DETAIL = 2;\n}\n\n//\nmessage CampusHomeRcmdTopic {\n  //\n  ModuleTitle title = 1;\n  //\n  repeated TopicItem topic = 2;\n}\n\n//\nmessage CampusInfo {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  string desc = 3;\n  //\n  int64 online = 4;\n  //\n  string url = 5;\n}\n\n//\nmessage CampusLabel {\n  //\n  string text = 1;\n  //\n  string url = 2;\n  //\n  string desc = 3;\n}\n\n//\nmessage CampusMateLikeListReply {\n  //\n  repeated ModuleAuthor list = 1;\n}\n\n//\nmessage CampusMateLikeListReq {\n  //\n  int64 dynamic_id = 1;\n  //\n  CampusReqFromType from_type = 2;\n}\n\n//\nenum CampusMngAuditStatus {\n  //\n  campus_mng_audit_none = 0;\n  //\n  campus_mng_audit_in_process = 1;\n  //\n  campus_mng_audit_failed = 2;\n}\n\n//\nmessage CampusMngBadge {\n  //\n  string title = 1;\n  //\n  string badge_url = 2;\n  //\n  string upload_hint_msg = 3;\n}\n\n//\nmessage CampusMngBasicInfo {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  string hint_msg = 3;\n}\n\n//\nmessage CampusMngDetailReply {\n  //\n  repeated CampusMngItem items = 1;\n  //\n  string top_hint_bar_msg = 2;\n  //\n  string bottom_submit_hint_msg = 3;\n  //\n  int64 campus_id = 4;\n  //\n  string campus_name = 5;\n}\n\n//\nmessage CampusMngDetailReq {\n  //\n  int64 campus_id = 1;\n}\n\n//\nmessage CampusMngItem {\n  //\n  int32 audit_status = 1;\n  //\n  string audit_message = 2;\n  //\n  int32 item_type = 3;\n  //\n  string mng_item_id = 4;\n  //\n  bool is_del = 5;\n  // Oneof field:\n  oneof item {\n    //\n    CampusMngBasicInfo basic_info = 6;\n    //\n    CampusMngBadge badge = 7;\n    //\n    string slogan = 8;\n    //\n    CampusMngQuiz quiz = 9;\n  }\n}\n\n//\nenum CampusMngItemType {\n  //\n  campus_mng_none = 0;\n  //\n  campus_mng_basic_info = 1;\n  //\n  campus_mng_badge = 2;\n  //\n  campus_mng_slogan = 3;\n  //\n  campus_mng_quiz = 4;\n}\n\n//\nmessage CampusMngQuiz {\n  //\n  string title = 1;\n  //\n  CampusLabel more_label = 2;\n  //\n  string add_label = 3;\n  //\n  string submit_label = 4;\n  //\n  int64 quiz_count = 5;\n}\n\n//\nenum CampusMngQuizAction {\n  //\n  campus_mng_quiz_act_list = 0;\n  //\n  campus_mng_quiz_act_add = 1;\n  //\n  campus_mng_quiz_act_del = 2;\n}\n\n//\nmessage CampusMngQuizDetail {\n  //\n  int64 quiz_id = 1;\n  //\n  string question = 2;\n  //\n  string correct_answer = 3;\n  //\n  repeated string wrong_answer_list = 4;\n  //\n  int32 audit_status = 5;\n  //\n  string audit_message = 6;\n}\n\n//\nmessage CampusMngQuizOperateReply {\n  //\n  string toast = 1;\n  //\n  repeated CampusMngQuizDetail quiz = 2;\n  //\n  int64 quiz_total = 3;\n}\n\n//\nmessage CampusMngQuizOperateReq {\n  //\n  int32 action = 1;\n  //\n  int64 campus_id = 2;\n  //\n  repeated CampusMngQuizDetail quiz = 3;\n}\n\n//\nmessage CampusMngSlogan {\n  //\n  string title = 1;\n  //\n  string slogan = 2;\n  //\n  string input_hint_msg = 3;\n}\n\n//\nmessage CampusMngSubmitReply {\n  //\n  string toast = 1;\n}\n\n//\nmessage CampusMngSubmitReq {\n  //\n  int64 campus_id = 1;\n  //\n  repeated CampusMngItem modified_items = 2;\n}\n\n//\nmessage CampusNoticeInfo {\n  //\n  string title = 1;\n  //\n  string desc = 2;\n  //\n  CampusLabel button = 3;\n}\n\n//\nenum CampusOnlineStatus {\n  //\n  campus_online_offline = 0;\n  //\n  campus_online_online = 1;\n}\n\n//\nmessage CampusRcmdFeedReply {\n  //\n  repeated DynamicItem list = 1;\n  //\n  string toast = 2;\n  //\n  GuideBarInfo guide_bar = 3;\n  //\n  bool has_more = 4;\n  //\n  bool update = 5;\n}\n\n//\nmessage CampusRcmdFeedReq {\n  //\n  int64 campus_id = 1;\n  //\n  int32 first_time = 2;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 3;\n  //\n  int32 local_time = 4;\n  //\n  int32 page = 5;\n  //\n  int32 scroll = 6;\n  //\n  string view_dyn_id = 7;\n  //\n  CampusReqFromType from_type = 8;\n}\n\n//\nmessage CampusRcmdInfo {\n  //\n  string title = 1;\n  //\n  repeated CampusRcmdItem items = 2;\n}\n\n//\nmessage CampusRcmdItem {\n  //\n  string title = 1;\n  //\n  repeated RcmdItem items = 2;\n  //\n  int64 campus_id = 3;\n  //\n  CampusLabel entry_label = 4;\n}\n\n//\nmessage CampusRcmdReply {\n  //\n  CampusRcmdTop top = 1;\n  //\n  CampusRcmdInfo rcmd = 2;\n  //\n  CampusTop campus_top = 3;\n  //\n  int32 page_type = 4;\n  //\n  int32 jump_home_pop = 5;\n}\n\n//\nmessage CampusRcmdReq {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  double lat = 3;\n  //\n  double lng = 4;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 5;\n  //\n  CampusReqFromType from_type = 6;\n  //\n  CampusHomePageType page_type = 7;\n}\n\n//\nmessage CampusRcmdTop {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  string title = 3;\n  //\n  string desc = 4;\n  //\n  int32 type = 5;\n  //\n  RcmdTopButton button = 6;\n  //\n  CampusLabel switch_label = 7;\n  //\n  CampusLabel notice_label = 8;\n  //\n  string desc2 = 9;\n  //\n  string desc3 = 10;\n  //\n  CampusLabel invite_label = 11;\n  //\n  CampusLabel reserve_label = 12;\n  //\n  int64 reserve_number = 13;\n  //\n  int64 max_reserve = 14;\n  //\n  CampusLabel school_label = 15;\n  //\n  CampusLabel mng_label = 16;\n  //\n  CampusHomeRcmdTopic rcmd_topic = 17;\n  //\n  bool audit_before_open = 18;\n  //\n  string audit_message = 19;\n}\n\n//\nmessage CampusRecommendReply {\n  //\n  repeated RcmdItem items = 1;\n  //\n  bool has_more = 2;\n}\n\n//\nmessage CampusRecommendReq {\n  //\n  int64 campus_id = 1;\n  //\n  int64 page_no = 2;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 3;\n  //\n  //CampusRcmdReqFrom from = 4;\n}\n\n//\nmessage CampusRedDotReply {\n  //\n  int32 red_dot = 1;\n}\n\n//\nmessage CampusRedDotReq {\n  //\n  int64 campus_id = 1;\n  //\n  CampusReqFromType from_type = 2;\n}\n\n//\nenum CampusReqFromType {\n  //\n  DYNAMIC = 0;\n  //\n  HOME = 1;\n}\n\n//\nmessage CampusShowTabInfo {\n  //\n  string name = 1;\n  //\n  string url = 2;\n  //\n  int32 type = 3;\n  //\n  int32 red_dot = 4;\n  //\n  string icon_url = 5;\n}\n\n//\nmessage CampusSquareReply {\n  //\n  string title = 1;\n  //\n  repeated RcmdCampusBrief list = 2;\n  //\n  CampusLabel button = 3;\n}\n\n//\nmessage CampusSquareReq {\n  //\n  int64 campus_id = 1;\n  //\n  double lat = 2;\n  //\n  double lng = 3;\n}\n\n//\nenum CampusTabType {\n  campus_none = 0;      //\n  campus_school = 1;    //\n  campus_dynamic = 2;   //\n  campus_account = 3;   //\n  campus_billboard = 4; //\n  campus_topic = 5;     //\n  campues_other = 6;    //\n}\n\n//\nmessage CampusTop {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  repeated CampusShowTabInfo tabs = 3;\n  //\n  CampusLabel switch_label = 4;\n  //\n  string title = 5;\n  //\n  repeated CampusBannerInfo banner = 6;\n  //\n  CampusLabel invite_label = 7;\n  //\n  CampusNoticeInfo notice = 8;\n  //\n  TopicSquareInfo topic_square = 9;\n  //\n  string campus_badge = 10;\n  //\n  string campus_background = 11;\n  //\n  string campus_motto = 12;\n  //\n  CampusLabel mng_entry = 13;\n  //\n  string campus_intro = 14;\n  //\n  string campus_name_link = 15;\n  //\n  string bottom_left_text = 16;\n}\n\n//\nmessage CampusTopicRcmdFeedReply {\n  //\n  repeated DynamicItem list = 1;\n  //\n  string toast = 2;\n  //\n  bool has_more = 3;\n  //\n  string offset = 4;\n  //\n  IconButton join_discuss = 5;\n}\n\n//\nmessage CampusTopicRcmdFeedReq {\n  //\n  int64 campus_id = 1;\n  //\n  string offset = 2;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 3;\n  //\n  int32 local_time = 4;\n  //\n  CampusReqFromType from_type = 5;\n}\n\n//\nmessage CardParagraph {\n  //\n  ModuleAdditional additional_card = 1;\n  //\n  string biz_id = 3;\n  //\n  LinkNodeType biz_type = 2;\n}\n\n// 动态卡片列表\nmessage CardVideoDynList {\n  // 动态列表\n  repeated DynamicItem list = 1;\n  // 更新的动态数\n  int64 update_num = 2;\n  // 历史偏移\n  string history_offset = 3;\n  // 更新基础信息\n  string update_baseline = 4;\n  // 是否还有更多数据\n  bool has_more = 5;\n}\n\n// 视频页-我的追番\nmessage CardVideoFollowList {\n  // 查看全部(跳转链接)\n  string view_all_link = 1;\n  // 追番列表\n  repeated FollowListItem list = 2;\n}\n\n// 视频页-最近访问\nmessage CardVideoUpList {\n  // 标题展示文案\n  string title = 1;\n  // up主列表\n  repeated UpListItem list = 2;\n  // 服务端生成的透传上报字段\n  string footprint = 3;\n  // 直播数\n  int32 show_live_num = 4;\n  // 跳转label\n  UpListMoreLabel more_label = 5;\n  // 标题开关(综合页)\n  int32 title_switch = 6;\n  // 是否展示右上角查看更多label\n  bool show_more_label = 7;\n  // 是否在快速消费页查看更多按钮\n  bool show_in_personal = 8;\n  // 是否展示右侧查看更多按钮\n  bool show_more_button = 9;\n  //\n  repeated UpListItem list_second = 10;\n}\n\n//\nmessage ChannelInfo {\n  //\n  int64 channel_id = 1;\n  //\n  string channel_name = 2;\n  //\n  string desc = 3;\n  //\n  bool is_atten = 4;\n  //\n  string type_icon = 5;\n  //\n  repeated RcmdItem items = 6;\n  //\n  string icon = 7;\n  //\n  string jump_uri = 8;\n}\n\n// 评论外露展示项\nmessage CmtShowItem {\n  // 用户mid\n  int64 uid = 1;\n  // 用户昵称\n  string uname = 2;\n  // 点击跳转链接\n  string uri = 3;\n  // 评论内容\n  string comment = 4;\n}\n\n//\nmessage Colors {\n  //\n  string color_day = 1;\n  //\n  string color_night = 2;\n}\n\n// 精选评论区\nmessage CommentDetail {\n  // 该功能能不能用\n  bool can_modify = 1;\n  // up关闭评论区功能 1允许关闭 0允许开放\n  // 精选评论区功能 1允许停止评论精选 0允许评论精选\n  int64 status = 2;\n}\n\n//\nmessage CommonShareCardInfo {\n  //\n  int64 sketch_id = 1;\n  //\n  int64 biz_type = 2;\n  //\n  int64 biz_id = 3;\n}\n\n//\nmessage Config {\n  //\n  bool story_vertical_exp = 1;\n  //\n  int64 detail_view_bits = 2;\n}\n\n//\nenum CoverIcon {\n  cover_icon_none = 0;    // 占位 啥都不展示\n  cover_icon_play = 1;    // 播放icon\n  cover_icon_danmaku = 2; //\n  cover_icon_up = 3;      //\n  cover_icon_vt = 4;      // ? 竖屏模式 icon\n}\n\n//\nmessage CoverIconWithText {\n  //\n  int32 icon = 1;\n  //\n  string text = 2;\n}\n\n// 装扮卡片-粉丝勋章信息\nmessage DecoCardFan {\n  // 是否是粉丝\n  int32 is_fan = 1;\n  // 数量\n  int32 number = 2;\n  // 数量 str\n  string number_str = 3;\n  // 颜色\n  string color = 4;\n}\n\n// 装扮卡片\nmessage DecorateCard {\n  // 装扮卡片id\n  int64 id = 1;\n  // 装扮卡片链接\n  string card_url = 2;\n  // 装扮卡片点击跳转链接\n  string jump_url = 3;\n  // 粉丝样式\n  DecoCardFan fan = 4;\n}\n\n// 文本描述\nmessage Description {\n  // 文本内容\n  string text = 1;\n  // 文本类型\n  DescType type = 2;\n  // 点击跳转链接\n  string uri = 3;\n  // emoji类型\n  EmojiType emoji_type = 4;\n  // 商品类型\n  string goods_type = 5;\n  // 前置Icon\n  string icon_url = 6;\n  // icon_name\n  string icon_name = 7;\n  // 资源ID\n  string rid = 8;\n  // 商品卡特殊字段\n  ModuleDescGoods goods = 9;\n  // 文本原始文案\n  string orig_text = 10;\n  //\n  int32 emoji_size = 11;\n  //\n  EmojiSizeSpec emoji_size_spec = 12;\n}\n\n// 文本类型\nenum DescType {\n  desc_type_none = 0;         // 占位\n  desc_type_text = 1;         // 文本\n  desc_type_aite = 2;         // @\n  desc_type_lottery = 3;      // 抽奖\n  desc_type_vote = 4;         // 投票\n  desc_type_topic = 5;        // 话题\n  desc_type_goods = 6;        // 商品\n  desc_type_bv = 7;           // bv\n  desc_type_av = 8;           // av\n  desc_type_emoji = 9;        // 表情\n  desc_type_user = 10;        // 外露用户\n  desc_type_cv = 11;        // 专栏\n  desc_type_vc = 12;        // 小视频\n  desc_type_web = 13;        // 网址\n  desc_type_taobao = 14;     // 淘宝\n  desc_type_mail = 15;        // 邮箱\n  desc_type_ogv_season = 16;  // 番剧season\n  desc_type_ogv_ep = 17;      // 番剧ep\n  desc_type_search_word = 18; //\n}\n\n\n// 尺寸信息\nmessage Dimension {\n  //\n  int64 height = 1;\n  //\n  int64 width = 2;\n  //\n  int64 rotate = 3;\n}\n\n//\nenum DisableState {\n  highlight = 0; // 高亮\n  gary = 1;      // 置灰(按钮不可点击)\n}\n\n// 动态通用附加卡-follow/取消follow-响应\nmessage DynAdditionCommonFollowReply {\n  //\n  AdditionalButtonStatus status = 1;\n}\n\n// 动态通用附加卡-follow/取消follow-请求\nmessage DynAdditionCommonFollowReq {\n  //\n  AdditionalButtonStatus status = 1;\n  //\n  string dyn_id = 2;\n  //\n  string card_type = 3;\n}\n\n// 最近访问-个人feed流列表-返回\nmessage DynAllPersonalReply {\n  // 动态列表\n  repeated DynamicItem list = 1;\n  // 偏移量\n  string offset = 2;\n  // 是否还有更多数据\n  bool has_more = 3;\n  // 已读进度\n  string read_offset = 4;\n  // 关注状态\n  Relation relation = 5;\n  // 顶部预约卡\n  TopAdditionUP addition_up = 6;\n  //\n  string title = 7;\n  //\n  string title_sub = 8;\n}\n\n// 最近访问-个人feed流列表-请求\nmessage DynAllPersonalReq {\n  // 被访问者的 UID\n  int64 host_uid = 1;\n  // 偏移量 第一页可传空\n  string offset = 2;\n  // 标明下拉几次\n  int32 page = 3;\n  // 是否是预加载 默认是1；客户端预加载。1：是预加载，不更新已读进度，不会影响小红点；0：非预加载，更新已读进度\n  int32 is_preload = 4;\n  // 秒开参数 新版本废弃，统一使用player_args\n  PlayurlParam playurl_param = 5;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 6;\n  // 服务端生成的透传上报字段\n  string footprint = 7;\n  // 来源\n  string from = 8;\n  // 秒开用\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 9;\n  //\n  string personal_extra = 10;\n}\n\n// 动态综合页-响应\nmessage DynAllReply {\n  // 卡片列表\n  DynamicList dynamic_list = 1;\n  // 顶部up list\n  CardVideoUpList up_list = 2;\n  // 话题广场\n  TopicList topic_list = 3;\n  // 无关注推荐\n  Unfollow unfollow = 4;\n  // 分区UP推荐\n  DynRegionRcmd region_rcmd = 5;\n  //\n  Config config = 6;\n}\n\n// 动态综合页-请求\nmessage DynAllReq {\n  // 透传 update_baseline\n  string update_baseline = 1;\n  // 透传 history_offset\n  string offset = 2;\n  // 向下翻页数\n  int32 page = 3;\n  // 刷新方式 1向上刷新 2向下翻页\n  Refresh refresh_type = 4;\n  // 秒开参数 新版本废弃，统一使用player_args\n  PlayurlParam playurl_param = 5;\n  // 综合页当前更新的最大值\n  string assist_baseline = 6;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 7;\n  // 推荐up主入参(new的时候传)\n  RcmdUPsParam rcmd_ups_param = 8;\n  // 广告参数\n  AdParam ad_param = 9;\n  // 是否冷启\n  int32 cold_start = 10;\n  // 来源\n  string from = 11;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 12;\n  //\n  int64 tab_recall_uid = 13;\n  //\n  int32 tab_recall_type = 14;\n}\n\n// 最近访问-标记已读-请求\nmessage DynAllUpdOffsetReq {\n  // 被访问者的UID\n  int64 host_uid = 1;\n  // 用户已读进度\n  string read_offset = 2;\n  // 服务端生成的透传上报字段\n  string footprint = 3;\n  //\n  string personal_extra = 4;\n}\n\n\n// 动态卡片\nmessage DynamicItem {\n  // 动态卡片类型\n  DynamicType card_type = 1;\n  // 转发类型下，源卡片类型\n  DynamicType item_type = 2;\n  // 模块内容\n  repeated Module modules = 3;\n  // 操作相关字段\n  Extend extend = 4;\n  // 该卡片下面是否含有折叠卡\n  int32 has_fold = 5;\n  // 透传到客户端的埋点字段。\n  string server_info = 6;\n}\n\n//动态卡片列表\nmessage DynamicList {\n  // 动态列表\n  repeated DynamicItem list = 1;\n  // 更新的动态数\n  int64 update_num = 2;\n  // 历史偏移\n  string history_offset = 3;\n  // 更新基础信息\n  string update_baseline = 4;\n  // 是否还有更多数据\n  bool has_more = 5;\n}\n\n// 枚举-动态类型\nenum DynamicType {\n  dyn_none = 0;          // 占位\n  forward = 1;           // 转发\n  av = 2;                // 稿件: ugc、小视频、短视频、UGC转PGC\n  pgc = 3;               // pgc：番剧、PGC番剧、PGC电影、PGC电视剧、PGC国创、PGC纪录片\n  courses = 4;           // 付费更新批次\n  fold = 5;              // 折叠\n  word = 6;              // 纯文字\n  draw = 7;              // 图文\n  article = 8;           // 专栏 原仅phone端\n  music = 9;             // 音频 原仅phone端\n  common_square = 10;    // 通用卡 方形\n  common_vertical = 11;  // 通用卡 竖形\n  live = 12;             // 直播卡 只有转发态\n  medialist = 13;        // 播单 原仅phone端 只有转发态\n  courses_season = 14;   // 付费更新批次 只有转发态\n  ad = 15;               // 广告卡\n  applet = 16;           // 小程序卡\n  subscription = 17;     // 订阅卡\n  live_rcmd = 18;        // 直播推荐卡\n  banner = 19;           // 通栏\n  ugc_season = 20;       // 合集卡\n  subscription_new = 21; // 新订阅卡\n  story = 22;            //\n  topic_rcmd = 23;       //\n  cour_up = 24;          //\n  topic_set = 25;        //\n  notice = 26;           //\n  text_notice = 27;      //\n}\n\n// 动态详情页-响应\nmessage DynDetailReply {\n  // 动态详情\n  DynamicItem item = 1;\n}\n\n// 动态详情页-请求\nmessage DynDetailReq {\n  // up主uid\n  int64 uid = 1;\n  // 动态ID\n  string dynamic_id = 2;\n  // 动态类型\n  int64 dyn_type = 3;\n  // 业务方资源id\n  int64 rid = 4;\n  // 广告参数\n  AdParam ad_param = 5;\n  // From来源\n  string from = 6;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 7;\n  // 分享id\n  string share_id = 8;\n  // 分享类型\n  // 1:文字 2:图片 3:链接 4:视频 5:音频\n  int32 share_mode = 9;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 10;\n  // pattern\n  string pattern = 11;\n  //\n  Config config = 12;\n}\n\n// 批量动态id获取动态详情-响应\nmessage DynDetailsReply {\n  // 动态列表\n  repeated DynamicItem list = 1;\n}\n\n// 批量动态id获取动态详情-请求\nmessage DynDetailsReq {\n  // 动态id\n  string dynamic_ids = 1;\n  // 秒开参数 新版本废弃，统一使用player_args\n  PlayurlParam playurl_param = 2;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 3;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 4;\n  //\n  Config config = 5;\n}\n\n// 动态小卡类型\nenum DynExtendType {\n  dyn_ext_type_none = 0;     // 占位\n  dyn_ext_type_topic = 1;    // 话题小卡\n  dyn_ext_type_lbs = 2;      // lbs小卡\n  dyn_ext_type_hot = 3;      // 热门小卡\n  dyn_ext_type_game = 4;     // 游戏小卡\n  dyn_ext_type_common = 5;   // 通用小卡\n  dyn_ext_type_biliCut = 6;  // 必剪小卡\n  dyn_ext_type_ogv = 7;      // ogv小卡\n  dyn_ext_type_auto_ogv = 8; // 自动附加ogv小卡\n}\n\n// 动态发布生成临时卡-响应\nmessage DynFakeCardReply {\n  // 动态卡片\n  DynamicItem item = 1;\n}\n\n// 动态发布生成临时卡-请求\nmessage DynFakeCardReq {\n  //卡片内容json string\n  string content = 1;\n}\n\n//\nmessage DynFeatureGate {\n  //\n  bool enhanced_interaction = 1;\n}\n\n//\nmessage DynFriendReply {\n  //\n  repeated DynamicItem dyn_list = 1;\n  //\n  bool has_more = 2;\n  //\n  string offset = 3;\n}\n\n//\nmessage DynFriendReq {\n  //\n  string offset = 1;\n  //\n  int32 local_time = 2;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 3;\n}\n\n// 轻浏览-响应\nmessage DynLightReply {\n  // 卡片列表\n  DynamicList dynamic_list = 1;\n}\n\n// 轻浏览-请求\nmessage DynLightReq {\n  // 透传 history_offset\n  string history_offset = 1;\n  // 向下翻页数\n  int32 page = 2;\n  // 来源\n  string from = 3;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 4;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 5;\n  //\n  int32 from_type = 6;\n  //\n  int64 fake_uid = 7;\n}\n\n// 查看更多-列表-响应\nmessage DynMixUpListViewMoreReply {\n  //\n  repeated MixUpListItem items = 1;\n  //\n  string  search_default_text = 2;\n  // 排序类型列表\n  repeated SortType  sort_types = 3;\n  // 是否展示更多的排序策略\n  bool show_more_sort_types = 4;\n  // 默认排序策略\n  int32 default_sort_type = 5;\n}\n\n// 查看更多-请求\nmessage DynMixUpListViewMoreReq {\n  // 排序策略\n  // 1:推荐排序 2:最常访问 3:最近关注，其他值为默认排序\n  int32 sort_type = 1;\n}\n\n// 动态模块类型\nenum DynModuleType {\n  module_none = 0;               // 占位\n  module_author = 1;             // 发布人模块\n  module_dispute = 2;            // 争议小黄条\n  module_desc = 3;               // 描述文案\n  module_dynamic = 4;            // 动态卡片\n  module_forward = 5;            // 转发模块\n  module_likeUser = 6;           // 点赞用户(废弃)\n  module_extend = 7;             // 小卡模块\n  module_additional = 8;         // 附加卡\n  module_stat = 9;               // 计数信息\n  module_fold = 10;              // 折叠\n  module_comment = 11;           // 评论外露(废弃)\n  module_interaction = 12;       // 外露交互模块(点赞、评论)\n  module_author_forward = 13;    // 转发卡的发布人模块\n  module_ad = 14;                // 广告卡模块\n  module_banner = 15;            // 通栏模块\n  module_item_null = 16;         // 获取物料失败模块\n  module_share_info = 17;        // 分享组件\n  module_recommend = 18;         // 相关推荐模块\n  module_stat_forward = 19;      // 转发卡计数信息\n  module_top = 20;               // 顶部模块\n  module_bottom = 21;            // 底部模块\n  module_story = 22;             //\n  module_topic = 23;             //\n  module_topic_details_ext = 24; //\n  module_top_tag = 25;           //\n  module_topic_brief = 26;       //\n  module_title = 27;             //\n  module_button = 28;\n  module_notice = 29;\n  module_opus_summary = 30;\n  module_copyright = 31;\n  module_paragraph = 32;\n  module_blocked = 33;\n  module_text_notice = 34;\n  module_opus_collection = 35;\n}\n\n// 推荐页-响应\nmessage DynRcmdReply {\n  // 推荐页返回参数\n  DynRegionRcmd region_rcmd = 1;\n  //\n  DynamicList dynamic_list = 2;\n}\n\n// 推荐页-请求\nmessage DynRcmdReq {\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 1;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 2;\n  //\n  int64 fake_uid = 3;\n  //\n  bool is_refresh = 4;\n}\n\n// 关注推荐up主换一换-响应\nmessage DynRcmdUpExchangeReply {\n  // 无关注推荐\n  Unfollow unfollow = 1;\n}\n\n// 关注推荐up主换一换-请求\nmessage DynRcmdUpExchangeReq {\n  // 登录用户id\n  int64 uid = 1;\n  // 上一次不感兴趣的ts，单位：秒；该字段透传给搜索\n  int64 dislikeTs = 2;\n  // 需要与服务端确认或参照客户端现有参数\n  string from = 3;\n}\n\n// 推荐页返回参数\nmessage DynRegionRcmd {\n  // 分区推荐项目列表\n  repeated DynRegionRcmdItem items = 1;\n  // 分区聚类推荐选项\n  RcmdOption opts = 2;\n}\n\n// 分区推荐项目\nmessage DynRegionRcmdItem {\n  // 分区id\n  int64 rid = 1;\n  // 标题\n  string title = 2;\n  // 推荐模块\n  repeated ModuleRcmd items = 3;\n}\n\n//\nmessage DynScreenTab {\n  //\n  string title = 1;\n  //\n  string name = 2;\n  //\n  bool default_tab = 3;\n  //\n  bool strategy_show_on_entrance = 4;\n  //\n  bool strategy_show_on_refresh = 5;\n  //\n  bool strategy_show_on_pull_up = 6;\n}\n\n//\nmessage DynSearchReply {\n  //\n  SearchChannel channel_info = 1;\n  //\n  SearchTopic search_topic = 2;\n  //\n  SearchInfo search_info = 3;\n}\n\n//\nmessage DynSearchReq {\n  //\n  string keyword = 1;\n  //\n  int32 page = 2;\n  //\n  int32 local_time = 3;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 4;\n}\n\n//\nmessage DynServerDetailsReply {\n  //\n  map<int64, DynamicItem> items = 1;\n}\n\n//\nmessage DynServerDetailsReq {\n  //\n  int32 local_time = 2;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 3;\n  //\n  string mobi_app = 4;\n  //\n  string device = 5;\n  //\n  string buvid = 6;\n  //\n  int64 build = 7;\n  //\n  int64 mid = 8;\n  //\n  string platform = 9;\n  //\n  bool is_master = 10;\n  //\n  repeated int64 top_dynamic_ids = 11;\n}\n\n// 空间页动态-请求\nmessage DynSpaceReq {\n  // 被访问者，也就是空间主人的uid\n  int64 host_uid = 1;\n  // 动态偏移history_offset\n  string history_offset = 2;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 3;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 4;\n  // 向下翻页数，默认从1开始\n  int64 page = 5;\n  // 来源，空间页：space，直播tab：live\n  string from = 6;\n}\n\n// 空间页动态-响应\nmessage DynSpaceRsp {\n  // 卡片列表\n  repeated DynamicItem list = 1;\n  // 历史偏移\n  string history_offset = 2;\n  // 是否还有更多数据\n  bool has_more = 3;\n}\n\n//\nmessage DynSpaceSearchDetailsReply {\n  //\n  map<int64, DynamicItem> items = 1;\n}\n\n//\nmessage DynSpaceSearchDetailsReq {\n  //\n  repeated string search_words = 2;\n  //\n  int32 local_time = 3;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 4;\n  //\n  string mobi_app = 5;\n  //\n  string device = 6;\n  //\n  string buvid = 7;\n  //\n  int64 build = 8;\n  //\n  int64 mid = 9;\n  //\n  string platform = 10;\n  //\n  string ip = 11;\n  //\n  int32 net_type = 12;\n  //\n  int32 tf_type = 13;\n}\n\n//\nmessage DynTab {\n  //\n  string title = 1;\n  //\n  string uri = 2;\n  //\n  string bubble = 3;\n  //\n  int32 red_point = 4;\n  //\n  int64 city_id = 5;\n  //\n  int32 is_popup = 6;\n  //\n  Popup popup = 7;\n  //\n  bool default_tab = 8;\n  //\n  string sub_title = 9;\n  //\n  string anchor = 10;\n  //\n  string internal_test = 11;\n  //\n  int32 type = 12;\n  //\n  DynTab back_up = 13;\n}\n\n//\nmessage DynTabReply {\n  //\n  repeated DynTab dyn_tab = 1;\n  //\n  repeated DynScreenTab screen_tab = 2;\n}\n\n//\nmessage DynTabReq {\n  //\n  int32 teenagers_mode = 1;\n  //\n  CampusReqFromType from_type = 2;\n}\n\n// 动态点赞-请求\nmessage DynThumbReq {\n  // 用户uid\n  int64 uid = 1;\n  // 动态id\n  string dyn_id = 2;\n  // 动态类型(透传extend中的dyn_type)\n  int64 dyn_type = 3;\n  // 业务方资源id\n  string rid = 4;\n  // 点赞类型\n  ThumbType type = 5;\n}\n\n// 最近访问-个人feed流列表-响应\nmessage DynVideoPersonalReply {\n  // 动态列表\n  repeated DynamicItem list = 1;\n  // 偏移量\n  string offset = 2;\n  // 是否还有更多数据\n  bool has_more = 3;\n  // 已读进度\n  string read_offset = 4;\n  // 关注状态\n  Relation relation = 5;\n  // 顶部预约卡\n  TopAdditionUP addition_up = 6;\n  //\n  string title = 7;\n  //\n  string title_sub = 8;\n}\n\n// 最近访问-个人feed流列表-请求\nmessage DynVideoPersonalReq {\n  // 被访问者的 UID\n  int64 host_uid = 1;\n  // 偏移量 第一页可传空\n  string offset = 2;\n  // 标明下拉几次\n  int32 page = 3;\n  // 是否是预加载\n  int32 is_preload = 4;\n  // 秒开参数 新版本废弃，统一使用player_args\n  PlayurlParam playurl_param = 5;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 6;\n  // 服务端生成的透传上报字段\n  string footprint = 7;\n  // 来源\n  string from = 8;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 9;\n  //\n  int64 pegasus_avid = 10;\n  //\n  string personal_extra = 11;\n}\n\n// 动态视频页-响应\nmessage DynVideoReply {\n  // 卡片列表\n  CardVideoDynList dynamic_list = 1;\n  // 动态卡片\n  CardVideoUpList video_up_list = 2;\n  // 视频页-我的追番\n  CardVideoFollowList video_follow_list = 3;\n}\n\n// 动态视频页-请求\nmessage DynVideoReq {\n  // 透传 update_baseline\n  string update_baseline = 1;\n  // 透传 history_offset\n  string offset = 2;\n  // 向下翻页数\n  int32 page = 3;\n  // 刷新方式\n  // 1:向上刷新 2:向下翻页\n  Refresh refresh_type = 4;\n  // 秒开参数 新版本废弃，统一使用player_args\n  PlayurlParam playurl_param = 5;\n  // 综合页当前更新的最大值\n  string assist_baseline = 6;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 7;\n  // 来源\n  string from = 8;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 9;\n}\n\n// 最近访问-标记已读-请求\nmessage DynVideoUpdOffsetReq {\n  // 被访问者的UID\n  int64 host_uid = 1;\n  // 用户已读进度\n  string read_offset = 2;\n  // 服务端生成的透传上报字段\n  string footprint = 3;\n  //\n  string personal_extra = 4;\n}\n\n// 投票操作-响应\nmessage DynVoteReply {\n  // 投票详情\n  AdditionVote2 item = 1;\n  // 投票操作返回状态\n  string toast = 2;\n}\n\n// 投票操作-请求\nmessage DynVoteReq {\n  // 投票ID\n  int64 vote_id = 1;\n  // 选项索引数组\n  repeated int64 votes = 2;\n  // 状态\n  VoteStatus status = 3;\n  // 动态ID\n  string dynamic_id = 4;\n  // 是否分享\n  bool share = 5;\n}\n\n//\nmessage EmojiSizeSpec {\n  //\n  int64 width = 1;\n}\n\n// 表情包类型\nenum EmojiType {\n  emoji_none = 0; // 占位\n  emoji_old = 1;  // emoji旧类型\n  emoji_new = 2;  // emoji新类型\n  vip = 3;        // 大会员表情\n}\n\n//\nmessage EmoteNode {\n  //\n  string emote_url = 2;\n  //\n  EmoteSize emote_width = 3;\n  //\n  ImgInlineCfg inline_img_cfg = 5;\n  //\n  bool is_inline_img = 4;\n  //\n  WordNode raw_text = 1;\n}\n\n//\nmessage EmoteSize {\n  //\n  double width = 1;\n  //\n  int32 emoji_size = 2;\n}\n\n// 附加大卡-电竞卡样式\nenum EspaceStyle {\n  moba = 0; // moba类\n}\n\n// 扩展字段，用于动态部分操作使用\nmessage Extend {\n  // 动态id\n  string dyn_id_str = 1;\n  // 业务方id\n  string business_id = 2;\n  // 源动态id\n  string orig_dyn_id_str = 3;\n  // 转发卡：用户名\n  string orig_name = 4;\n  // 转发卡：图片url\n  string orig_img_url = 5;\n  // 转发卡：文字内容\n  repeated Description orig_desc = 6;\n  // 填充文字内容\n  repeated Description desc = 7;\n  // 被转发的源动态类型\n  DynamicType orig_dyn_type = 8;\n  // 分享到站外展示类型\n  string share_type = 9;\n  // 分享的场景\n  string share_scene = 10;\n  // 是否快速转发\n  bool is_fast_share = 11;\n  // r_type 分享和转发\n  int32 r_type = 12;\n  // 数据源的动态类型\n  int64 dyn_type = 13;\n  // 用户id\n  int64 uid = 14;\n  // 卡片跳转\n  string card_url = 15;\n  // 透传字段\n  google.protobuf.Any source_content = 16;\n  // 转发卡：用户头像\n  string orig_face = 17;\n  // 评论跳转\n  ExtendReply reply = 18;\n  //\n  string track_id = 19;\n  //\n  ModuleOpusSummary opus_summary = 20;\n  //\n  OnlyFansProperty only_fans_property = 21;\n  //\n  DynFeatureGate feature_gate = 22;\n  //\n  bool is_in_audit = 23;\n  //\n  map<string, string> history_report = 24;\n}\n\n// 评论扩展\nmessage ExtendReply {\n  // 基础跳转地址\n  string uri = 1;\n  // 参数部分\n  repeated ExtendReplyParam params = 2;\n}\n\n// 评论扩展参数部分\nmessage ExtendReplyParam {\n  // 参数名\n  string key = 1;\n  // 参数值\n  string value = 2;\n}\n\n// 动态-拓展小卡模块-通用小卡\nmessage ExtInfoCommon {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n  // poiType\n  int32 poi_type = 4;\n  // 类型\n  DynExtendType type = 5;\n  // 客户端埋点用\n  string sub_module = 6;\n  // 行动点文案\n  string action_text = 7;\n  // 行动点链接\n  string action_url = 8;\n  // 资源rid\n  int64 rid = 9;\n  // 轻浏览是否展示\n  bool is_show_light = 10;\n}\n\n// 动态-拓展小卡模块-游戏小卡\nmessage ExtInfoGame {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n}\n\n// 动态-拓展小卡模块-热门小卡\nmessage ExtInfoHot {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n}\n\n// 动态-拓展小卡模块-lbs小卡\nmessage ExtInfoLBS {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n  // poiType\n  int32 poi_type = 4;\n}\n\n// 动态-拓展小卡模块-ogv小卡\nmessage ExtInfoOGV {\n  // ogv小卡\n  repeated InfoOGV info_ogv = 1;\n}\n\n// 动态-拓展小卡模块-话题小卡\nmessage ExtInfoTopic {\n  // 标题-话题名\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n}\n\n//\nmessage FeedFilterReply {\n  //\n  string offset = 1;\n  //\n  bool has_more = 2;\n  //\n  repeated DynamicItem list = 3;\n}\n\n//\nmessage FeedFilterReq {\n  //\n  string offset = 1;\n  //\n  string tab = 2;\n  //\n  int32 local_time = 3;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 4;\n  //\n  AdParam ad_param = 5;\n  //\n  int32 cold_start = 6;\n  //\n  int64 page = 7;\n}\n\n//\nmessage FetchTabSettingReply {\n  //\n  int32 status = 1;\n}\n\n// 折叠类型\nenum FoldType {\n  FoldTypeZore = 0;     // 占位\n  FoldTypePublish = 1;  // 用户发布折叠\n  FoldTypeFrequent = 2; // 转发超频折叠\n  FoldTypeUnite = 3;    // 联合投稿折叠\n  FoldTypeLimit = 4;    // 动态受限折叠\n  FoldTypeTopicMerged = 5;\n}\n\n// 视频页-我的追番-番剧信息\nmessage FollowListItem {\n  // season_id\n  int64 season_id = 1;\n  // 标题\n  string title = 2;\n  // 封面图\n  string cover = 3;\n  // 跳转链接\n  string url = 4;\n  // new_ep\n  NewEP new_ep = 5;\n  // 子标题\n  string sub_title = 6;\n  // 卡片位次\n  int64 pos = 7;\n}\n\n//\nenum FollowType {\n  ft_not_follow = 0; //\n  ft_follow = 1;     //\n}\n\n// 动态-附加卡-商品卡-商品\nmessage GoodsItem {\n  // 图片\n  string cover = 1;\n  // schemaPackageName(Android用)\n  string schema_package_name = 2;\n  // 商品类型\n  // 1:淘宝 2:会员购\n  int32 source_type = 3;\n  // 跳转链接\n  string jump_url = 4;\n  // 跳转文案\n  string jump_desc = 5;\n  // 标题\n  string title = 6;\n  // 摘要\n  string brief = 7;\n  // 价格\n  string price = 8;\n  // item_id\n  int64 item_id = 9;\n  // schema_url\n  string schema_url = 10;\n  // open_white_list\n  repeated string open_white_list = 11;\n  // use_web_v2\n  bool user_web_v2 = 12;\n  // ad mark\n  string ad_mark = 13;\n  //\n  string app_name = 14;\n  //\n  GoodsJumpType jump_type = 15;\n}\n\n//\nenum GoodsJumpType {\n  goods_none = 0;\n  goods_schema = 1;\n  goods_url = 2;\n}\n\n//\nmessage GuideBarInfo {\n  //\n  int32 show = 1;\n  //\n  int32 page = 2;\n  //\n  int32 position = 3;\n  //\n  string desc = 4;\n  //\n  int32 jump_page = 5;\n  //\n  int32 jump_position = 6;\n}\n\n// 高亮文本\nmessage HighlightText {\n  // 展示文本\n  string text = 1;\n  // 高亮类型\n  HighlightTextStyle text_style = 2;\n  //\n  string jump_url = 3;\n  //\n  string icon = 4;\n}\n\n// 文本高亮枚举\nenum HighlightTextStyle {\n  style_none = 0;      // 默认\n  style_highlight = 1; // 高亮\n}\n\n//\nenum HomePageTabSttingStatus {\n  SETTING_INVALID = 0;\n  SETTING_OPEN = 1;\n  SETTING_CLOSE = 2;\n}\n\n//\nmessage HomeSubscribeReply {\n  //\n  int32 online = 1;\n}\n\n//\nmessage HomeSubscribeReq {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n}\n\n//\nmessage IconBadge {\n  //\n  string icon_bg_url = 1;\n  //\n  string text = 2;\n}\n\n//\nmessage IconButton {\n  //\n  string text = 1;\n  //\n  string icon_head = 2;\n  //\n  string icon_tail = 3;\n  //\n  string jump_uri = 4;\n}\n\n//\nenum IconResLocal {\n  ICON_RES_LOCAL_NONE = 0;\n  ICON_RES_LOCAL_LIVE = 1;\n}\n\n//\nmessage ImageSet {\n  //\n  string img_day = 1;\n  //\n  string img_dark = 2;\n}\n\n// 枚举-附加卡样式\nenum ImageStyle {\n  add_style_vertical = 0; //\n  add_style_square = 1;   //\n}\n\n//\nmessage ImgInlineCfg {\n  //\n  double width = 1;\n  //\n  double height = 2;\n  //\n  Colors color = 3;\n}\n\n// 动态-拓展小卡模块-ogv小卡-(one of 片单、榜单、分区)\nmessage InfoOGV {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n  // 客户端埋点用\n  string sub_module = 4;\n}\n\n//\nmessage InteractionFace {\n  //\n  int64 mid = 1;\n  //\n  string face = 2;\n}\n\n// 外露交互模块\nmessage InteractionItem {\n  // 外露模块类型\n  LocalIconType icon_type = 1;\n  // 外露模块文案\n  repeated Description desc = 2;\n  // 外露模块uri相关 根据type不同用法不同\n  string uri = 3;\n  // 动态id\n  string dynamic_id = 4;\n  // 评论mid\n  int64 comment_mid = 6;\n  //\n  repeated InteractionFace faces = 7;\n  //\n  InteractionStat stat = 8;\n  //\n  string icon = 9;\n}\n\n//\nmessage InteractionStat {\n  //\n  int64 like = 1;\n}\n\n//\nmessage LbsPoiDetail {\n  //\n  string poi = 1;\n  //\n  int64 type = 2;\n  //\n  repeated string base_pic = 3;\n  //\n  repeated string cover = 4;\n  //\n  string address = 5;\n  //\n  string title = 6;\n}\n\n//\nmessage LbsPoiReply {\n  //\n  bool has_more = 1;\n  //\n  string offset = 2;\n  //\n  LbsPoiDetail detail = 3;\n  //\n  repeated DynamicItem list = 4;\n}\n\n//\nmessage LbsPoiReq {\n  //\n  string poi = 1;\n  //\n  int64 type = 2;\n  //\n  string offset = 3;\n  //\n  int32 refresh_type = 4;\n  //\n  int32 local_time = 5;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 6;\n}\n\n//\nmessage LegacyTopicFeedReply {\n  //\n  repeated DynamicItem list = 1;\n  //\n  bool has_more = 2;\n  //\n  string offset = 3;\n  //\n  repeated SortType supported_sort_types = 4;\n  //\n  repeated SortType feed_card_filters = 5;\n}\n\n//\nmessage LegacyTopicFeedReq {\n  //\n  int64 topic_id = 1;\n  //\n  string topic_name = 2;\n  //\n  string offset = 3;\n  //\n  SortType sort_type = 4;\n  //\n  SortType card_filter = 5;\n}\n\n//\nenum LightFromType {\n  from_login = 0;   //\n  from_unlogin = 1; //\n}\n\n// 点赞动画\nmessage LikeAnimation {\n  // 开始动画\n  string begin = 1;\n  // 过程动画\n  string proc = 2;\n  // 结束动画\n  string end = 3;\n  // id\n  int64 like_icon_id = 4;\n}\n\n// 点赞拓展信息\nmessage LikeInfo {\n  // 点赞动画\n  LikeAnimation animation = 1;\n  // 是否点赞\n  bool is_like = 2;\n}\n\n// 点赞列表-响应\nmessage LikeListReply {\n  // 用户模块列表\n  repeated ModuleAuthor list = 1;\n  // 是否还有更多数据\n  bool has_more = 2;\n  // 点赞总数\n  int64 total_count = 3;\n}\n\n// 点赞列表-请求\nmessage LikeListReq {\n  // 动态ID\n  string dynamic_id = 1;\n  // 动态类型\n  int64 dyn_type = 2;\n  // 业务方资源id\n  int64 rid = 3;\n  //上一页最后一个uid\n  int64 uid_offset = 4;\n  // 下拉页数\n  int32 page = 5;\n}\n\n// 点赞用户\nmessage LikeUser {\n  // 用户mid\n  int64 uid = 1;\n  // 用户昵称\n  string uname = 2;\n  // 点击跳转链接\n  string uri = 3;\n}\n\n//\nmessage LineParagraph {\n  //\n  MdlDynDrawItem pic = 1;\n}\n\n//\nmessage LinkNode {\n  //\n  WordNode show_text = 1;\n  //\n  string link = 2;\n  //\n  string icon = 3;\n  //\n  string icon_suffix = 4;\n  //\n  string link_type = 5;\n  //\n  LinkNodeType link_type_enum = 6;\n  //\n  string biz_id = 7;\n  //\n  int64 timestamp = 8;\n  //\n  GoodsItem goods_item = 9;\n  //\n  NoteVideoTS note_video_ts = 10;\n}\n\n//\nenum LinkNodeType {\n  INVALID = 0;\n  VIDEO = 1;\n  RESERVE = 2;\n  VOTE = 3;\n  LIVE = 4;\n  LOTTERY = 5;\n  MATCH = 6;\n  GOODS = 7;\n  OGV_SS = 8;\n  OGV_EP = 9;\n  MANGA = 10;\n  CHEESE = 11;\n  VIDEO_TS = 12;\n  AT = 13;\n  HASH_TAG = 14;\n  ARTICLE = 15;\n  URL = 16;\n  MAIL = 17;\n  LBS = 18;\n  ACTIVITY = 19;\n  ATTACH_CARD_OFFICIAL_ACTIVITY = 20;\n  GAME = 21;\n  DECORATION = 22;\n  UP_TOPIC = 23;\n  UP_ACTIVITY = 24;\n  UP_MAOER = 25;\n  MEMBER_GOODS = 26;\n  OPENMALL_UP_ITEMS = 27;\n  SEARCH = 28;\n}\n\n// 直播信息\nmessage LiveInfo {\n  // 是否在直播\n  // 0:未直播 1:正在直播 (废弃)\n  int32 is_living = 1;\n  // 跳转链接\n  string uri = 2;\n  // 直播状态\n  LiveState live_state = 3;\n}\n\n//\nmessage LivePendant {\n  //\n  string text = 1;\n  //\n  string icon = 2;\n  //\n  int64 pendant_id = 3;\n}\n\n// 直播状态\nenum LiveState {\n  live_none = 0;     // 未直播\n  live_live = 1;     // 直播中\n  live_rotation = 2; // 轮播中\n}\n\n// 外露模块类型\nenum LocalIconType {\n  local_icon_comment = 0; //\n  local_icon_like = 1;    //\n  local_icon_avatar = 2;\n  local_icon_cover = 3;\n  local_icon_like_and_forward = 4;\n}\n\n// 动态-附加卡-电竞卡-战队\nmessage MatchTeam {\n  // 战队ID\n  int64 id = 1;\n  // 战队名\n  string name = 2;\n  // 战队图标\n  string cover = 3;\n  // 日间色值\n  string color = 4;\n  // 夜间色值\n  string night_color = 5;\n}\n\n//\nenum MdlBlockedStyle {\n  BLOCKED_STYLE_DEFAULT = 0;\n  BLOCKED_STYLE_IN_AUDIT = 1;\n  BLOCKED_STYLE_ONLY_FANS_LIST = 2;\n  BLOCKED_STYLE_ONLY_FANS_VIDEO = 3;\n}\n\n// 动态列表渲染部分-详情模块-小程序/小游戏\nmessage MdlDynApplet {\n  // 小程序id\n  int64 id = 1;\n  // 跳转地址\n  string uri = 2;\n  // 主标题\n  string title = 4;\n  // 副标题\n  string sub_title = 5;\n  // 封面图\n  string cover = 6;\n  // 小程序icon\n  string icon = 7;\n  // 小程序标题\n  string label = 8;\n  // 按钮文案\n  string button_title = 9;\n}\n\n// 动态-详情模块-稿件\nmessage MdlDynArchive {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 秒开地址\n  string uri = 3;\n  // 视频封面展示项 1\n  string cover_left_text_1 = 4;\n  // 视频封面展示项 2\n  string cover_left_text_2 = 5;\n  // 封面视频展示项 3\n  string cover_left_text_3 = 6;\n  // avid\n  int64 avid = 7;\n  // cid\n  int64 cid = 8;\n  // 视频源类型\n  MediaType media_type = 9;\n  // 尺寸信息\n  Dimension dimension = 10;\n  // 角标，多个角标之前有间距\n  repeated VideoBadge badge = 11;\n  // 是否能够自动播放\n  bool  can_play = 12;\n  // stype\n  VideoType stype = 13;\n  // 是否PGC\n  bool isPGC = 14;\n  // inline播放地址\n  string inlineURL = 15;\n  // PGC的epid\n  int64 EpisodeId = 16;\n  // 子类型\n  int32 SubType = 17;\n  // PGC的ssid\n  int64 PgcSeasonId = 18;\n  // 播放按钮\n  string play_icon = 19;\n  // 时长\n  int64 duration = 20;\n  // 跳转地址\n  string jump_url = 21;\n  // 番剧是否为预览视频\n  bool is_preview = 22;\n  // 新角标，多个角标之前没有间距\n  repeated VideoBadge badge_category = 23;\n  // 当前是否是pgc正片\n  bool is_feature = 24;\n  // 是否是预约召回\n  ReserveType reserve_type = 25;\n  // bvid\n  string bvid = 26;\n  // 播放数\n  int64 view = 27;\n  //\n  bool show_premiere_badge = 28;\n  //\n  bool premiere_card = 29;\n  //\n  bool show_progress = 30;\n  //\n  int64 part_duration = 31;\n  //\n  int64 part_progress = 32;\n}\n\n// 动态列表渲染部分-详情模块-专栏模块\nmessage MdlDynArticle {\n  // 专栏id\n  int64 id = 1;\n  // 跳转地址\n  string uri = 2;\n  // 标题\n  string title = 3;\n  // 文案部分\n  string desc = 4;\n  // 配图\n  repeated string covers = 5;\n  // 阅读量标签\n  string label = 6;\n  // 模板类型\n  int32 templateID = 7;\n}\n\n//\nmessage MdlDynChargingArchive {\n  //\n  MdlDynArchive archive_info = 1;\n  //\n  bool has_permission = 2;\n  //\n  bool can_inline = 3;\n  //\n  string charging_bundle_name = 4;\n  //\n  int64 cfg_preview_end_toast_countdown = 5;\n  //\n  int64 cfg_normal_inline_toast_duration = 6;\n  //\n  OneLineText video_bottom_text_upper = 7;\n  //\n  OneLineText video_bottom_text_lower = 8;\n  //\n  string archive_cover = 9;\n  //\n  string archive_title = 10;\n  //\n  IconButton act_btn = 11;\n  //\n  OneLineText text_normal_inline_toast = 12;\n  //\n  OneLineText text_append_preview_end_toast = 13;\n}\n\n// 动态列表渲染部分-详情模块-通用\nmessage MdlDynCommon {\n  // 物料id\n  int64 oid = 1;\n  // 跳转地址\n  string uri = 2;\n  // 标题\n  string title = 3;\n  // 描述 漫画卡标题下第一行\n  string desc = 4;\n  // 封面\n  string cover = 5;\n  // 标签1 漫画卡标题下第二行\n  string label = 6;\n  // 所属业务类型\n  int32 bizType = 7;\n  // 镜像数据ID\n  int64 sketchID = 8;\n  // 卡片样式\n  MdlDynCommonType style = 9;\n  // 角标\n  repeated VideoBadge badge = 10;\n  //\n  AdditionalButton button = 11;\n}\n\n//\nenum MdlDynCommonType {\n  mdl_dyn_common_none = 0;    //\n  mdl_dyn_common_square = 1;  //\n  mdl_dyn_common_vertica = 2; //\n}\n\n// 动态-详情模块-付费课程批次\nmessage MdlDynCourBatch {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 跳转地址\n  string uri = 3;\n  // 展示项 1(本集标题)\n  string text_1 = 4;\n  // 展示项 2(更新了多少个视频)\n  string text_2 = 5;\n  // 角标\n  VideoBadge badge = 6;\n  // 播放按钮\n  string play_icon = 7;\n  //\n  bool can_play = 8;\n  //\n  bool is_preview = 9;\n  //\n  string cover_left_text_1 = 10;\n  //\n  string cover_left_text_2 = 11;\n  //\n  string cover_left_text_3 = 12;\n  //\n  int64 avid = 13;\n  //\n  int64 cid = 14;\n  //\n  int64 epid = 15;\n  //\n  int64 duration = 16;\n  //\n  int64 season_id = 17;\n}\n\n// 动态-详情模块-付费课程系列\nmessage MdlDynCourSeason {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 跳转地址\n  string uri = 3;\n  // 展示项 1(更新信息)\n  string text_1 = 4;\n  // 描述信息\n  string desc = 5;\n  // 角标\n  VideoBadge badge = 6;\n  // 播放按钮\n  string play_icon = 7;\n  //\n  bool can_play = 8;\n  //\n  bool is_preview = 9;\n  //\n  int64 avid = 10;\n  //\n  int64 cid = 11;\n  //\n  int64 epid = 12;\n  //\n  int64 duration = 13;\n  //\n  int64 season_id = 14;\n}\n\n//\nmessage MdlDynCourUp {\n  //\n  string title = 1;\n  //\n  string desc = 2;\n  //\n  string cover = 3;\n  //\n  string uri = 4;\n  //\n  string text_1 = 5;\n  //\n  VideoBadge badge = 6;\n  //\n  string play_icon = 7;\n  //\n  bool can_play = 8;\n  //\n  bool is_preview = 9;\n  //\n  int64 avid = 10;\n  //\n  int64 cid = 11;\n  //\n  int64 epid = 12;\n  //\n  int64 duration = 13;\n  //\n  int64 season_id = 14;\n}\n\n// 动态列表渲染部分-详情模块-图文模块\nmessage MdlDynDraw {\n  // 图片\n  repeated MdlDynDrawItem items = 1;\n  // 跳转地址\n  string uri = 2;\n  // 图文ID\n  int64 id = 3;\n  //\n  bool is_draw_first = 4;\n  //\n  bool is_big_cover = 5;\n  //\n  bool is_article_cover = 6;\n}\n\n// 动态列表渲染部分-详情模块-图文\nmessage MdlDynDrawItem {\n  // 图片链接\n  string src = 1;\n  // 图片宽度\n  int64 width = 2;\n  // 图片高度\n  int64 height = 3;\n  // 图片大小\n  float size = 4;\n  // 图片标签\n  repeated MdlDynDrawTag tags = 5;\n}\n\n// 动态列表渲染部分-详情模块-图文-标签\nmessage MdlDynDrawTag {\n  // 标签类型\n  MdlDynDrawTagType type = 1;\n  // 标签详情\n  MdlDynDrawTagItem item = 2;\n}\n\n// 动态列表部分-详情模块-图文-标签详情\nmessage MdlDynDrawTagItem {\n  // 跳转链接\n  string url = 1;\n  // 标签内容\n  string text = 2;\n  // 坐标-x\n  int64 x = 3;\n  // 坐标-y\n  int64 y = 4;\n  // 方向\n  int32 orientation = 5;\n  // 来源\n  // 0:未知 1:淘宝 2:自营\n  int32 source = 6;\n  // 商品id\n  int64 item_id = 7;\n  // 用户mid\n  int64 mid = 8;\n  // 话题id\n  int64 tid = 9;\n  // lbs信息\n  string poi = 10;\n  // 商品标签链接\n  string schema_url = 11;\n}\n\n// 图文标签类型\nenum MdlDynDrawTagType {\n  mdl_draw_tag_none = 0;   // 占位\n  mdl_draw_tag_common = 1; // 普通标签\n  mdl_draw_tag_goods = 2;  // 商品标签\n  mdl_draw_tag_user = 3;   // 用户昵称\n  mdl_draw_tag_topic = 4;  // 话题名称\n  mdl_draw_tag_lbs = 5;    // lbs标签\n}\n\n// 动态列表渲染部分-详情模块-转发模块\nmessage MdlDynForward {\n  // 动态转发核心模块 套娃\n  DynamicItem item = 1;\n  // 透传类型\n  // 0:分享 1:转发\n  int32 rtype = 2;\n}\n\n// 动态列表渲染部分-详情模块-直播\nmessage MdlDynLive {\n  // 房间号\n  int64 id = 1;\n  // 跳转地址\n  string uri = 2;\n  // 直播间标题\n  string title = 3;\n  // 直播间封面\n  string cover = 4;\n  // 标题1 例: 陪伴学习\n  string cover_label = 5;\n  // 标题2 例: 54.6万人气\n  string cover_label2 = 6;\n  // 直播状态\n  LiveState live_state = 7;\n  // 直播角标\n  VideoBadge badge = 8;\n  // 是否是预约召回\n  ReserveType reserve_type = 9;\n}\n\n// 动态列表渲染部分-详情模块-直播推荐\nmessage MdlDynLiveRcmd {\n  // 直播数据\n  string content = 1;\n  // 是否是预约召回\n  ReserveType reserve_type = 2;\n  //\n  LivePendant pendant = 3;\n}\n\n// 动态列表渲染部分-详情模块-播单\nmessage MdlDynMedialist {\n  // 播单id\n  int64 id = 1;\n  // 跳转地址\n  string uri = 2;\n  // 主标题\n  string title = 3;\n  // 副标题\n  string sub_title = 4;\n  // 封面图\n  string cover = 5;\n  // 封面类型\n  int32 cover_type = 6;\n  // 角标\n  VideoBadge badge = 7;\n}\n\n// 动态列表渲染部分-详情模块-音频模块\nmessage MdlDynMusic {\n  // 音频id\n  int64 id = 1;\n  // 跳转地址\n  string uri = 2;\n  // upId\n  int64 up_id = 3;\n  // 歌名\n  string title = 4;\n  // 专辑封面\n  string cover = 5;\n  // 展示项1\n  string label1 = 6;\n  // upper\n  string upper = 7;\n}\n\n// 动态-详情模块-pgc\nmessage MdlDynPGC {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 秒开地址\n  string uri = 3;\n  // 视频封面展示项 1\n  string cover_left_text_1 = 4;\n  // 视频封面展示项 2\n  string cover_left_text_2 = 5;\n  // 封面视频展示项 3\n  string cover_left_text_3 = 6;\n  // cid\n  int64 cid = 7;\n  // season_id\n  int64 season_id = 8;\n  // epid\n  int64 epid = 9;\n  // aid\n  int64 aid = 10;\n  // 视频源类型\n  MediaType media_type = 11;\n  // 番剧类型\n  VideoSubType sub_type = 12;\n  // 番剧是否为预览视频\n  bool is_preview = 13;\n  // 尺寸信息\n  Dimension dimension = 14;\n  // 角标，多个角标之前有间距\n  repeated VideoBadge badge = 15;\n  // 是否能够自动播放\n  bool  can_play = 16;\n  // season\n  PGCSeason season = 17;\n  // 播放按钮\n  string play_icon = 18;\n  // 时长\n  int64 duration = 19;\n  // 跳转地址\n  string jump_url = 20;\n  // 新角标，多个角标之前没有间距\n  repeated VideoBadge badge_category = 21;\n  // 当前是否是pgc正片\n  bool is_feature = 22;\n}\n\n//\nmessage MdlDynShareChargingQA {\n  //\n  ImageSet background_img = 1;\n  //\n  ImageSet left_icon_img = 2;\n  //\n  string title = 3;\n  //\n  IconButton jump_button = 4;\n  //\n  string uri = 5;\n  //\n  CommonShareCardInfo share_card_meta_info = 6;\n  //\n  string title_prefix_bold = 7;\n}\n\n// 动态列表渲染部分-详情模块-订阅卡\nmessage MdlDynSubscription {\n  // 卡片物料id\n  int64 id = 1;\n  // 广告创意id\n  int64 ad_id = 2;\n  // 跳转地址\n  string uri = 3;\n  // 标题\n  string title = 4;\n  // 封面图\n  string cover = 5;\n  // 广告标题\n  string ad_title = 6;\n  // 角标\n  VideoBadge badge = 7;\n  // 小提示\n  string tips = 8;\n}\n\n// 动态新附加卡\nmessage MdlDynSubscriptionNew {\n  //样式类型\n  MdlDynSubscriptionNewStyle style = 1;\n  // 新订阅卡数据\n  oneof item {\n    //\n    MdlDynSubscription dyn_subscription = 2;\n    // 直播推荐\n    MdlDynLiveRcmd dyn_live_rcmd = 3;\n  }\n}\n\n//\nenum MdlDynSubscriptionNewStyle {\n  mdl_dyn_subscription_new_style_nont = 0; // 占位\n  mdl_dyn_subscription_new_style_live = 1; // 直播\n  mdl_dyn_subscription_new_style_draw = 2; // 图文\n}\n\n//\nmessage MdlDynTopicSet {\n  //\n  repeated TopicItem topics = 1;\n  //\n  IconButton more_btn = 2;\n  //\n  int64 topic_set_id = 3;\n  //\n  int64 push_id = 4;\n}\n\n// 动态列表渲染部分-UGC合集\nmessage MdlDynUGCSeason {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 秒开地址\n  string uri = 3;\n  // 视频封面展示项 1\n  string cover_left_text_1 = 4;\n  // 视频封面展示项 2\n  string cover_left_text_2 = 5;\n  // 封面视频展示项 3\n  string cover_left_text_3 = 6;\n  // 卡片物料id\n  int64 id = 7;\n  // inline播放地址\n  string inlineURL = 8;\n  // 是否能够自动播放\n  bool  can_play = 9;\n  // 播放按钮\n  string play_icon = 10;\n  // avid\n  int64 avid = 11;\n  // cid\n  int64 cid = 12;\n  // 尺寸信息\n  Dimension dimension = 13;\n  // 时长\n  int64 duration = 14;\n  // 跳转地址\n  string jump_url = 15;\n}\n\n// 播放器类型\nenum MediaType {\n  MediaTypeNone = 0; // 本地\n  MediaTypeUGC = 1;  // UGC\n  MediaTypePGC = 2;  // PGC\n  MediaTypeLive = 3; // 直播\n  MediaTypeVCS = 4;  // 小视频\n}\n\n// 查看更多-列表单条数据\nmessage MixUpListItem {\n  // 用户mid\n  int64 uid = 1;\n  // 特别关注\n  // 0:否 1:是\n  int32 special_attention = 2;\n  // 小红点状态\n  // 0:没有 1:有\n  int32 reddot_state = 3;\n  // 直播信息\n  MixUpListLiveItem live_info = 4;\n  // 昵称\n  string name = 5;\n  // 头像\n  string face = 6;\n  // 认证信息\n  OfficialVerify official = 7;\n  // 大会员信息\n  VipInfo vip = 8;\n  // 关注状态\n  Relation relation = 9;\n  //\n  int32 permire_state = 10;\n  //\n  string uri = 11;\n}\n\nmessage MixUpListLiveItem {\n  // 直播状态\n  // 0:未直播 1:直播中\n  bool status = 1;\n  // 房间号\n  int64 room_id = 2;\n  // 跳转地址\n  string uri = 3;\n}\n\n// 动态模块\nmessage Module {\n  // 类型\n  DynModuleType module_type = 1;\n  oneof module_item {\n    // 用户模块 1\n    ModuleAuthor module_author = 2;\n    // 争议黄条模块 2\n    ModuleDispute module_dispute = 3;\n    // 动态正文模块 3\n    ModuleDesc module_desc = 4;\n    // 动态卡模块 4\n    ModuleDynamic module_dynamic = 5;\n    // 点赞外露(废弃)\n    ModuleLikeUser module_likeUser = 6;\n    // 小卡模块 6\n    ModuleExtend module_extend = 7;\n    // 大卡模块 5\n    ModuleAdditional module_additional = 8;\n    // 计数模块 8\n    ModuleStat module_stat = 9;\n    // 折叠模块 9\n    ModuleFold module_fold = 10;\n    // 评论外露(废弃)\n    ModuleComment module_comment = 11;\n    // 外露交互模块(点赞、评论) 7\n    ModuleInteraction module_interaction = 12;\n    // 转发卡-原卡用户模块\n    ModuleAuthorForward module_author_forward = 13;\n    // 广告卡\n    ModuleAd module_ad = 14;\n    // 通栏\n    ModuleBanner module_banner = 15;\n    // 获取物料失败\n    ModuleItemNull module_item_null = 16;\n    // 分享组件\n    ModuleShareInfo module_share_info = 17;\n    // 相关推荐模块\n    ModuleRecommend module_recommend = 18;\n    // 顶部模块\n    ModuleTop module_top = 19;\n    // 底部模块\n    ModuleButtom module_buttom = 20;\n    // 转发卡计数模块\n    ModuleStat module_stat_forward = 21;\n    //\n    ModuleStory module_story = 22;\n    //\n    ModuleTopic module_topic = 23;\n    //\n    ModuleTopicDetailsExt module_topic_details_ext = 24;\n    //\n    ModuleTopTag module_top_tag = 25;\n    //\n    ModuleTopicBrief module_topic_brief = 26;\n    //\n    ModuleTitle module_title = 27;\n    //\n    ModuleButton module_button = 28;\n    //\n    ModuleNotice module_notice = 29;\n    //\n    ModuleOpusSummary module_opus_summary = 30;\n    //\n    ModuleCopyright module_copyright = 31;\n    //\n    ModuleParagraph module_paragraph = 32;\n    //\n    ModuleBlocked module_blocked = 33;\n    //\n    ModuleTextNotice module_text_notice = 34;\n    //\n    ModuleOpusCollection module_opus_collection = 35;\n  }\n}\n\n// 动态列表-用户模块-广告卡\nmessage ModuleAd {\n  // 广告透传信息\n  google.protobuf.Any source_content = 1;\n  // 用户模块\n  ModuleAuthor module_author = 2;\n  //\n  int32 ad_content_type = 3;\n  //\n  string cover_left_text1 = 4;\n  //\n  string cover_left_text2 = 5;\n  //\n  string cover_left_text3 = 6;\n}\n\n// 动态-附加卡模块\nmessage ModuleAdditional {\n  // 类型\n  AdditionalType type = 1;\n  oneof item {\n    // 废弃\n    AdditionalPGC pgc = 2;\n    //\n    AdditionGoods goods = 3;\n    // 废弃\n    AdditionVote vote = 4;\n    //\n    AdditionCommon common = 5;\n    //\n    AdditionEsport esport = 6;\n    // 投票\n    AdditionVote2 vote2 = 8;\n    //\n    AdditionUgc  ugc = 9;\n    // up主预约发布卡\n    AdditionUP up = 10;\n    //\n    AdditionArticle article = 12;\n    //\n    AdditionLiveRoom live = 13;\n  }\n  // 附加卡物料ID\n  int64 rid = 7;\n  //\n  bool need_write_calender = 11;\n}\n\n// 动态-发布人模块\nmessage ModuleAuthor {\n  // 用户mid\n  int64 mid = 1;\n  // 时间标签\n  string ptime_label_text = 2;\n  // 用户详情\n  UserInfo author = 3;\n  // 装扮卡片\n  DecorateCard decorate_card = 4;\n  // 点击跳转链接\n  string uri = 5;\n  // 右侧操作区域 - 三点样式\n  repeated ThreePointItem tp_list = 6;\n  // 右侧操作区域样式枚举\n  ModuleAuthorBadgeType badge_type = 7;\n  // 右侧操作区域 - 按钮样式\n  ModuleAuthorBadgeButton badge_button = 8;\n  // 是否关注\n  // 1:关注 0:不关注 默认0，注：点赞列表使用，其他场景不使用该字段\n  int32 attend = 9;\n  // 关注状态\n  Relation relation = 10;\n  // 右侧操作区域 - 提权样式\n  Weight weight = 11;\n  // 是否展示关注\n  bool show_follow = 12;\n  // 是否置顶\n  bool is_top = 13;\n  // ip属地\n  string ptime_location_text = 14;\n  //\n  bool show_level = 15;\n  //\n  OnlyFans only_fans = 16;\n}\n\n// 动态列表渲染部分-用户模块-按钮\nmessage ModuleAuthorBadgeButton {\n  // 图标\n  string icon = 1;\n  // 文案\n  string title = 2;\n  // 状态\n  int32 state = 3;\n  // 物料ID\n  int64 id = 4;\n}\n\n// 右侧操作区域样式枚举\nenum ModuleAuthorBadgeType {\n  module_author_badge_type_none = 0;       // 占位\n  module_author_badge_type_threePoint = 1; // 三点\n  module_author_badge_type_button = 2;     // 按钮类型\n  module_author_badge_type_weight = 3;     // 提权\n}\n\n// 动态列表-用户模块-转发模板\nmessage ModuleAuthorForward {\n  // 展示标题\n  repeated ModuleAuthorForwardTitle title = 1;\n  // 源卡片跳转链接\n  string url = 2;\n  // 用户uid\n  int64 uid = 3;\n  // 时间标签\n  string ptime_label_text = 4;\n  // 是否展示关注\n  bool show_follow = 5;\n  // 源up主头像\n  string face_url = 6;\n  // 双向关系\n  Relation relation = 7;\n  // 右侧操作区域 - 三点样式\n  repeated ThreePointItem tp_list = 8;\n}\n\n// 动态列表-用户模块-转发模板-title部分\nmessage ModuleAuthorForwardTitle {\n  // 文案\n  string text = 1;\n  // 跳转链接\n  string url = 2;\n}\n\n// 动态列表-通栏\nmessage ModuleBanner {\n  // 模块标题\n  string title = 1;\n  // 卡片类型\n  ModuleBannerType type = 2;\n  // 卡片\n  oneof item{\n    ModuleBannerUser user = 3;\n  }\n  // 不感兴趣文案\n  string dislike_text = 4;\n  // 不感兴趣图标\n  string dislike_icon = 5;\n}\n\n// 动态列表-通栏类型\nenum ModuleBannerType {\n  module_banner_type_none = 0; //\n  module_banner_type_user = 1; //\n}\n\n// 动态通栏-用户\nmessage ModuleBannerUser {\n  // 卡片列表\n  repeated ModuleBannerUserItem list = 1;\n}\n\n// 动态通栏-推荐用户卡\nmessage ModuleBannerUserItem {\n  // up主头像\n  string face = 1;\n  // up主昵称\n  string name = 2;\n  // up主uid\n  int64 uid = 3;\n  // 直播状态\n  LiveState live_state = 4;\n  // 认证信息\n  OfficialVerify official = 5;\n  // 大会员信息\n  VipInfo vip = 6;\n  // 标签信息\n  string label = 7;\n  // 按钮\n  AdditionalButton button = 8;\n  // 跳转地址\n  string uri = 9;\n  //\n  Relation relation = 10;\n}\n\n//\nmessage ModuleBlocked {\n  //\n  ImageSet icon = 1;\n  //\n  ImageSet bg_img = 2;\n  //\n  string hint_message = 3;\n  //\n  IconButton act_btn = 4;\n  //\n  MdlBlockedStyle block_style = 5;\n  //\n  string sub_hint_message = 6;\n  //\n  OneLineText video_bottom_text_upper = 7;\n  //\n  OneLineText video_bottom_text_lower = 8;\n  //\n  string archive_title = 9;\n  //\n  OneLineText hint_message_one_line = 10;\n}\n\n// 底部模块\nmessage ModuleButtom {\n  enum InteractionIcon {\n    ICON_INVALID = 0;\n    ICON_FORWARD = 1;\n    ICON_COMMENT = 2;\n    ICON_FAVORITE = 3;\n    ICON_LIKE = 4;\n  }\n  // 计数模块\n  ModuleStat module_stat = 1;\n  //\n  bool comment_box = 2;\n  //\n  string comment_box_msg = 3;\n  //\n  repeated InteractionIcon interaction_icons = 4;\n  //\n  repeated InteractionFace faces = 5;\n}\n\n//\nmessage ModuleButton {\n  //\n  IconButton btn = 1;\n}\n\n// 评论外露模块\nmessage ModuleComment {\n  // 评论外露展示项\n  repeated CmtShowItem cmtShowItem = 1;\n}\n\n//\nmessage ModuleCopyright {\n  //\n  string left_text = 1;\n  //\n  string right_text = 2;\n}\n\n// 动态-描述文字模块\nmessage ModuleDesc {\n  // 描述信息(已按高亮拆分)\n  repeated Description desc = 1;\n  // 点击跳转链接\n  string jump_uri = 2;\n  // 文本原本\n  string text = 3;\n}\n\n// 正文商品卡参数\nmessage ModuleDescGoods {\n  // 商品类型\n  // 1:淘宝 2:会员购\n  int32 source_type = 1;\n  // 跳转链接\n  string jump_url = 2;\n  // schema_url\n  string schema_url = 3;\n  // item_id\n  int64 item_id = 4;\n  // open_white_list\n  repeated string open_white_list = 5;\n  // use_web_v2\n  bool user_web_v2 = 6;\n  // ad mark\n  string ad_mark = 7;\n  // schemaPackageName(Android用)\n  string schema_package_name = 8;\n  //\n  int32 jump_type = 9;\n  //\n  string app_name = 10;\n}\n\n// 动态-争议小黄条模块\nmessage ModuleDispute {\n  // 标题\n  string title = 1;\n  // 描述内容\n  string desc = 2;\n  // 跳转链接\n  string uri = 3;\n}\n\n// 动态-详情模块\nmessage ModuleDynamic {\n  // 类型\n  ModuleDynamicType type = 1;\n  oneof module_item {\n    //稿件\n    MdlDynArchive dyn_archive = 2;\n    //pgc\n    MdlDynPGC dyn_pgc = 3;\n    //付费课程-系列\n    MdlDynCourSeason dyn_cour_season = 4;\n    //付费课程-批次\n    MdlDynCourBatch dyn_cour_batch = 5;\n    //转发卡\n    MdlDynForward dyn_forward = 6;\n    //图文\n    MdlDynDraw dyn_draw = 7;\n    //专栏\n    MdlDynArticle dyn_article = 8;\n    //音频\n    MdlDynMusic dyn_music = 9;\n    //通用卡方\n    MdlDynCommon dyn_common = 10;\n    //直播卡\n    MdlDynLive dyn_common_live = 11;\n    //播单\n    MdlDynMedialist dyn_medialist = 12;\n    //小程序卡\n    MdlDynApplet dyn_applet = 13;\n    //订阅卡\n    MdlDynSubscription dyn_subscription = 14;\n    //直播推荐卡\n    MdlDynLiveRcmd dyn_live_rcmd = 15;\n    //UGC合集\n    MdlDynUGCSeason dyn_ugc_season = 16;\n    //订阅卡\n    MdlDynSubscriptionNew dyn_subscription_new = 17;\n    //课程\n    MdlDynCourUp dyn_cour_batch_up = 18;\n    //话题集合\n    MdlDynTopicSet dyn_topic_set = 19;\n    //充电稿件\n    MdlDynChargingArchive dyn_charging_archive = 20;\n    //\n    MdlDynShareChargingQA dyn_share_charging_qa = 21;\n  }\n}\n\n// 动态详情模块类型\nenum ModuleDynamicType {\n  mdl_dyn_archive = 0;           // 稿件\n  mdl_dyn_pgc = 1;               // pgc\n  mdl_dyn_cour_season = 2;       // 付费课程-系列\n  mdl_dyn_cour_batch = 3;        // 付费课程-批次\n  mdl_dyn_forward = 4;           // 转发卡\n  mdl_dyn_draw = 5;              // 图文\n  mdl_dyn_article = 6;           // 专栏\n  mdl_dyn_music = 7;             // 音频\n  mdl_dyn_common = 8;            // 通用卡方\n  mdl_dyn_live = 9;              // 直播卡\n  mdl_dyn_medialist = 10;        // 播单\n  mdl_dyn_applet = 11;           // 小程序卡\n  mdl_dyn_subscription = 12;     // 订阅卡\n  mdl_dyn_live_rcmd = 13;        // 直播推荐卡\n  mdl_dyn_ugc_season = 14;       // UGC合集\n  mdl_dyn_subscription_new = 15; // 订阅卡\n  mdl_dyn_cour_batch_up = 16;    // 课程\n  mdl_dyn_topic_set = 17;        // 话题集合\n}\n\n// 动态-小卡模块\nmessage ModuleExtend {\n  // 详情\n  repeated ModuleExtendItem extend = 1;\n  // 模块整体跳转uri\n  string uri = 2; // 废弃\n}\n\n// 动态-拓展小卡模块\nmessage ModuleExtendItem {\n  // 类型\n  DynExtendType type = 1;\n  // 卡片详情\n  oneof extend {\n    // 废弃\n    ExtInfoTopic ext_info_topic = 2;\n    // 废弃\n    ExtInfoLBS ext_info_lbs = 3;\n    // 废弃\n    ExtInfoHot ext_info_hot = 4;\n    // 废弃\n    ExtInfoGame ext_info_game = 5;\n    //\n    ExtInfoCommon ext_info_common = 6;\n    //\n    ExtInfoOGV ext_info_ogv = 7;\n  }\n}\n\n// 动态-折叠模块\nmessage ModuleFold {\n  // 折叠分类\n  FoldType fold_type = 1;\n  // 折叠文案\n  string text = 2;\n  // 被折叠的动态\n  string fold_ids = 3;\n  // 被折叠的用户信息\n  repeated UserInfo fold_users = 4;\n  //\n  TopicMergedResource topic_merged_resource = 5;\n}\n\n// 外露交互模块\nmessage ModuleInteraction {\n  // 外露交互模块\n  repeated InteractionItem interaction_item = 1;\n}\n\n// 获取物料失败模块\nmessage ModuleItemNull {\n  // 图标\n  string icon = 1;\n  // 文案\n  string text = 2;\n}\n\n// 动态-点赞用户模块\nmessage ModuleLikeUser {\n  // 点赞用户\n  repeated LikeUser like_users = 1;\n  // 文案\n  string display_text = 2;\n}\n\n//\nmessage ModuleNotice {\n  //\n  string identity = 1;\n  //\n  string icon = 2;\n  //\n  string title = 3;\n  //\n  string url = 4;\n  //\n  int32 notice_type = 5;\n}\n\n//\nmessage ModuleOpusCollection {\n  //\n  OpusCollection collection_info = 1;\n  //\n  string title_upper = 2;\n  //\n  string title = 3;\n  //\n  string title_prefix_icon = 4;\n  //\n  string total_text = 5;\n}\n\n//\nmessage ModuleOpusSummary {\n  //\n  Paragraph title = 1;\n  //\n  Paragraph summary = 2;\n  //\n  string summary_jump_btn_text = 3;\n  //\n  repeated MdlDynDrawItem covers = 4;\n}\n\n//\nmessage ModuleParagraph {\n  //\n  Paragraph paragraph = 1;\n  //\n  bool is_article_title = 2;\n  //\n  ParaSpacing para_spacing = 3;\n}\n\n// 推荐模块\nmessage ModuleRcmd {\n  // 用户头像\n  RcmdAuthor author = 1;\n  // 推荐卡片列表\n  repeated RcmdItem items = 2;\n  // 透传到客户端的埋点字段\n  string server_info = 3;\n}\n\n// 相关推荐模块\nmessage ModuleRecommend {\n  // 模块标题\n  string module_title = 1;\n  // 图片\n  string image = 2;\n  // 标签\n  string tag = 3;\n  // 标题\n  string title = 4;\n  // 跳转链接\n  string jump_url = 5;\n  // 序列化的广告信息\n  repeated google.protobuf.Any ad = 6;\n}\n\n// 分享模块\nmessage ModuleShareInfo {\n  // 展示标题\n  string title = 1;\n  // 分享组件列表\n  repeated ShareChannel share_channels = 2;\n  // share_origin\n  string share_origin = 3;\n  // 业务id\n  string oid = 4;\n  // sid\n  string sid = 5;\n}\n\n// 动态-计数模块\nmessage ModuleStat {\n  // 转发数\n  int64 repost = 1;\n  // 点赞数\n  int64 like = 2;\n  // 评论数\n  int64 reply = 3;\n  // 点赞拓展信息\n  LikeInfo like_info = 4;\n  // 禁评\n  bool no_comment = 5;\n  // 禁转\n  bool no_forward = 6;\n  // 点击评论跳转链接\n  string reply_url = 7;\n  // 禁评文案\n  string no_comment_text = 8;\n  // 禁转文案\n  string no_forward_text = 9;\n}\n\n//\nmessage ModuleStory {\n  //\n  string title = 1;\n  //\n  repeated StoryItem items = 2;\n  //\n  bool show_publish_entrance = 3;\n  //\n  int64 fold_state = 4;\n  //\n  string uri = 5;\n  //\n  string cover = 6;\n  //\n  string publish_text = 7;\n}\n\n//\nmessage ModuleTextNotice {\n  //\n  OneLineText notice = 1;\n}\n\n//\nmessage ModuleTitle {\n  //\n  string title = 1;\n  //\n  IconButton right_btn = 2;\n  //\n  int32 title_style = 3;\n}\n\n// 顶部模块\nmessage ModuleTop {\n  // 三点模块\n  repeated ThreePointItem tp_list = 1;\n  //\n  MdlDynArchive archive = 2;\n  //\n  ModuleAuthor author = 3;\n  //\n  bool hidden_nav_bar = 4;\n}\n\n//\nmessage ModuleTopic {\n  //\n  int64 id = 1;\n  //\n  string name = 2;\n  //\n  string url = 3;\n}\n\n//\nmessage ModuleTopicBrief {\n  //\n  TopicItem topic = 1;\n}\n\n//\nmessage ModuleTopicDetailsExt {\n  //\n  string comment_guide = 1;\n}\n\n//\nmessage ModuleTopTag {\n  //\n  string tag_name = 1;\n}\n\n// 认证名牌\nmessage Nameplate {\n  // nid\n  int64 nid = 1;\n  // 名称\n  string name = 2;\n  // 图片地址\n  string image = 3;\n  // 小图地址\n  string image_small = 4;\n  // 等级\n  string level = 5;\n  // 获取条件\n  string condition = 6;\n}\n\nenum NetworkType {\n  NT_UNKNOWN = 0; //\n  WIFI = 1;       //\n  CELLULAR = 2;   //\n  OFFLINE = 3;    //\n  OTHERNET = 4;   //\n  ETHERNET = 5;   //\n}\n\n// 最新ep\nmessage NewEP {\n  // 最新话epid\n  int32 id = 1;\n  // 更新至XX话\n  string index_show = 2;\n  // 更新剧集的封面\n  string cover = 3;\n}\n\n//\nmessage NFTInfo {\n  //\n  NFTRegionType region_type = 1;\n  //\n  string region_icon = 2;\n  //\n  NFTShowStatus region_show_status = 3;\n}\n\n//\nenum NFTRegionType {\n  nft_region_default = 0;\n  nft_region_mainlang = 1;\n  nft_region_gat = 2;\n}\n\n//\nenum NFTShowStatus {\n  nft_show_default = 0;\n  nft_show_zoominmainlang = 1;\n  nft_show_raw = 2;\n}\n\n// 空响应\nmessage NoReply {\n\n}\n\n// 空请求\nmessage NoReq {\n\n}\n\n//\nmessage NoteVideoTS {\n  //\n  int64 cid = 1;\n  //\n  int64 oid_type = 2;\n  //\n  int64 status = 3;\n  //\n  int64 index = 4;\n  //\n  int64 seconds = 5;\n  //\n  int64 cid_count = 6;\n  //\n  string key = 7;\n  //\n  int64 epid = 9;\n  //\n  string title = 8;\n  //\n  string desc = 10;\n}\n\n//\nmessage OfficialAccountInfo {\n  //\n  UserInfo author = 1;\n  //\n  int64 mid = 2;\n  //\n  string uri = 3;\n  //\n  Relation relation = 4;\n  //\n  string desc_text1 = 5;\n  //\n  string desc_text2 = 6;\n}\n\n//\nmessage OfficialAccountsReply {\n  //\n  repeated OfficialAccountInfo items = 1;\n  //\n  bool has_more = 2;\n  //\n  int64 offset = 3;\n}\n\n//\nmessage OfficialAccountsReq {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  int64 offset = 3;\n}\n\n//\nmessage OfficialDynamicsReply {\n  //\n  repeated OfficialItem items = 1;\n  //\n  int64 offset = 2;\n  //\n  bool has_more = 3;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 4;\n}\n\n//\nmessage OfficialDynamicsReq {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  int64 offset = 3;\n}\n\nmessage OfficialItem {\n  int32 type = 1;\n  oneof rcmd_item {\n    OfficialRcmdArchive rcmd_archive = 2;\n    OfficialRcmdDynamic rcmd_dynamic = 3;\n  };\n}\n\n//\nmessage OfficialRcmdArchive {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string cover_right_text = 3;\n  //\n  int32 desc_icon1 = 4;\n  //\n  string desc_text1 = 5;\n  //\n  int32 desc_icon2 = 6;\n  //\n  string desc_text2 = 7;\n  //\n  string reason = 8;\n  //\n  bool show_three_point = 9;\n  //\n  string uri = 10;\n  //\n  int64 aid = 11;\n  //\n  int64 mid = 12;\n  //\n  string name = 13;\n  //\n  int64 dynamic_id = 14;\n  //\n  int64 cid = 15;\n}\n\n//\nmessage OfficialRcmdDynamic {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string cover_right_top_text = 3;\n  //\n  int32 desc_icon1 = 4;\n  //\n  string desc_text1 = 5;\n  //\n  int32 desc_icon2 = 6;\n  //\n  string desc_text2 = 7;\n  //\n  string reason = 8;\n  //\n  string uri = 9;\n  //\n  int64 dynamic_id = 10;\n  //\n  int64 mid = 11;\n  //\n  string user_name = 12;\n  //\n  int64 rid = 13;\n}\n\n// 认证信息\nmessage OfficialVerify {\n  // 127:未认证 0:个人 1:机构\n  int32 type = 1;\n  // 认证描述\n  string desc = 2;\n  // 是否关注\n  int32 is_atten = 3;\n}\n\n//\nmessage OneLineText {\n  //\n  repeated TextWithPriority texts = 1;\n}\n\n//\nmessage OnlyFans {\n  //\n  bool is_only_fans = 1;\n  //\n  IconBadge badge = 2;\n}\n\n//\nmessage OnlyFansProperty {\n  //\n  bool has_privilege = 1;\n  //\n  bool is_only_fans = 2;\n}\n\n//\nmessage OpusCollection {\n  //\n  int64 collection_id = 1;\n  //\n  OneLineText title = 2;\n  //\n  string detail_uri = 3;\n  //\n  string intro = 4;\n  //\n  repeated OpusCollectionItem all_items = 5;\n}\n\n//\nmessage OpusCollectionItem {\n  //\n  int64 opus_id = 1;\n  //\n  string title = 2;\n  //\n  string pub_time = 3;\n  //\n  string uri = 4;\n  //\n  bool is_selected_highlight = 5;\n}\n\n//\nmessage Paragraph {\n  //\n  message ListFormat {\n    //\n    int32 level = 1;\n    //\n    int32 order = 2;\n    //\n    string theme = 3;\n  }\n  enum ParagraphAlign {\n    LEFT = 0;\n    MIDDLE = 1;\n    RIGHT = 2;\n  }\n  //\n  message ParagraphFormat {\n    //\n    ParagraphAlign align = 1;\n    //\n    ListFormat list_format = 2;\n  }\n  //\n  enum ParagraphType {\n    INVALID = 0;\n    TEXT = 1;\n    PICTURES = 2;\n    LINE = 3;\n    REFERENCE = 4;\n    SORTED_LIST = 5;\n    UNSORTED_LIST = 6;\n    LINK_CARD = 7;\n  }\n  //\n  ParagraphType para_type = 1;\n  //\n  ParagraphFormat para_format = 2;\n  //\n  oneof content {\n    TextParagraph text = 3;\n    PicParagraph pic = 4;\n    LineParagraph line = 5;\n    CardParagraph link_card = 6;\n  }\n}\n\n//\nmessage ParaSpacing {\n  //\n  double spacing_before_para = 1;\n  //\n  double spacing_after_para = 2;\n  //\n  double line_spacing = 3;\n}\n\n// PGC单季信息\nmessage PGCSeason {\n  // 是否完结\n  int32 is_finish = 1;\n  // 标题\n  string title = 2;\n  // 类型\n  int32 type = 3;\n}\n\n//\nmessage PicParagraph {\n  //\n  enum PicParagraphStyle {\n    INVALID = 0;\n    NINE_CELL = 1;\n    BIG_SCROLL = 2;\n  }\n  //\n  MdlDynDraw pics = 1;\n  //\n  PicParagraphStyle style = 2;\n}\n\n// 秒开通用参数\nmessage PlayurlParam {\n  // 清晰度\n  int32 qn = 1;\n  // 流版本\n  int32 fnver = 2;\n  // 流类型\n  int32 fnval = 3;\n  // 是否强制使用域名\n  int32 force_host = 4;\n  // 是否4k\n  int32 fourk = 5;\n}\n\n//\nmessage Popup {\n  //\n  string title = 1;\n  //\n  string desc = 2;\n  //\n  string uri = 3;\n}\n\n//\nmessage RcmdArchive {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 视频封面展示项 1\n  CoverIcon cover_left_icon_1 = 3;\n  // 视频封面展示项 1\n  string cover_left_text_1 = 4;\n  // 秒开地址\n  string uri = 5;\n  // 是否PGC\n  bool is_pgc = 6;\n  // aid\n  int64 aid = 7;\n  //\n  IconBadge badge = 8;\n  //\n  int32 cover_left_icon2 = 9;\n  //\n  string cover_left_text2 = 10;\n  //\n  int32 cover_left_icon3 = 11;\n  //\n  string cover_left_text3 = 12;\n}\n\n// 推荐UP主用户模块\nmessage RcmdAuthor {\n  // 用户详情\n  UserInfo author = 1;\n  // 描述：粉丝数、推荐理由\n  string desc = 2;\n  // 关注状态\n  Relation relation = 3;\n}\n\n//\nmessage RcmdCampusBrief {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  string campus_badge = 4;\n  //\n  string url = 5;\n}\n\n// 推荐卡片列表\nmessage RcmdItem {\n  // 卡片类型\n  RcmdType type = 1;\n  // 卡片列表\n  oneof rcmd_item {\n    //\n    RcmdArchive rcmd_archive = 2;\n  }\n}\n\n// 分区聚类推荐选项\nmessage RcmdOption{\n  // 视频是否展示标题\n  bool show_title = 1;\n}\n\n//\nmessage RcmdReason {\n  //\n  string campus_name = 1;\n  //\n  RcmdReasonStyle style = 2;\n  //\n  string rcmd_reason = 3;\n  //\n  string up_name = 4;\n}\n\n//\nenum RcmdReasonStyle {\n  rcmd_reason_style_none = 0;\n  rcmd_reason_style_campus_nearby = 1;\n  rcmd_reason_style_campus_up = 2;\n  rcmd_reason_style_campus_near_up_mix = 3;\n}\n\n//\nmessage RcmdTopButton {\n  //\n  string text = 1;\n  //\n  string url = 2;\n}\n\n// 推荐模块数据类型\nenum RcmdType {\n  rcmd_archive = 0; // 稿件\n  rcmd_dynamic = 1; // 动态\n}\n\n// 推荐up主入参\nmessage RcmdUPsParam {\n  int64 dislike_ts = 1;\n}\n\nmessage ReactionListItem {\n  // 用户信息\n  UserInfo user = 1;\n  // 关注关系\n  Relation relation = 2;\n  // 显示文字\n  string act_text = 3;\n  //\n  string rcmd_reason = 4;\n}\n\n\n// 新版动态转发点赞列表-响应\nmessage ReactionListReply {\n  // 标题\n  string title = 1;\n  // 列表\n  repeated ReactionListItem list = 2;\n  // 偏移\n  string offset = 3;\n  // 是否还有更多\n  bool has_more = 4;\n}\n\n// 新版动态转发点赞列表-请求\nmessage ReactionListReq {\n  // 动态ID\n  int64 dynamic_id = 1;\n  // 动态类型\n  int64 dyn_type = 2;\n  // 业务方资源id\n  int64 rid = 3;\n  // 偏移,使用上一页回包中的offset字段；第一页不传。\n  string offset = 4;\n}\n\n// 刷新方式\nenum Refresh {\n  refresh_new = 0;     // 刷新列表\n  refresh_history = 1; // 请求历史\n}\n\n// 关注关系\nmessage Relation {\n  // 关注状态\n  RelationStatus status = 1;\n  // 关注\n  int32 is_follow = 2;\n  // 被关注\n  int32 is_followed = 3;\n  // 文案\n  string title = 4;\n}\n\n// 关注关系 枚举\nenum RelationStatus {\n  // 1-未关注 2-关注 3-被关注 4-互相关注 5-特别关注\n  relation_status_none = 0;\n  relation_status_nofollow = 1;\n  relation_status_follow = 2;\n  relation_status_followed = 3;\n  relation_status_mutual_concern = 4;\n  relation_status_special = 5;\n}\n\n// 转发列表-请求\nmessage RepostListReq {\n  // 动态ID\n  string dynamic_id = 1;\n  // 动态类型\n  int64 dyn_type = 2;\n  // 业务方资源id\n  int64 rid = 3;\n  // 偏移,使用上一页回包中的offset字段；第一页不传。\n  string offset = 4;\n  // 来源\n  string from = 5;\n  // 评论类型\n  RepostType repost_type = 6;\n}\n\n// 转发列表-响应\nmessage RepostListRsp {\n  // 列表\n  repeated DynamicItem list = 1;\n  // 偏移\n  string offset = 2;\n  // 是否还有更多\n  bool has_more = 3;\n  // 转发总数\n  int64 total_count = 4;\n  // 评论类型\n  RepostType repost_type = 5;\n}\n\n// 评论类型\nenum RepostType {\n  repost_hot = 0;     // 热门评论\n  repost_general = 1; // 普通评论\n}\n\n//\nenum ReserveRelationLotteryType {\n  reserve_relation_lottery_type_default = 0; //\n  reserve_relation_lottery_type_cron = 1;    //\n}\n\n//\nenum ReserveType {\n  reserve_none = 0;   // 占位\n  reserve_recall = 1; // 预约召回\n}\n\nenum RouterAction {\n  OPEN = 0;\n  EMBED = 1;\n}\n\n//\nmessage SchoolRecommendReply {\n  //\n  repeated CampusInfo items = 1;\n}\n\n//\nmessage SchoolRecommendReq {\n  //\n  float lat = 1;\n  //\n  float lng = 2;\n  //\n  CampusReqFromType from_type = 3;\n}\n\n//\nmessage SchoolSearchReply {\n  //\n  repeated CampusInfo items = 1;\n  //\n  SearchToast toast = 2;\n}\n\n//\nmessage SchoolSearchReq {\n  //\n  string keyword = 1;\n  //\n  CampusReqFromType from_type = 2;\n}\n\n//\nmessage SearchChannel {\n  //\n  string title = 1;\n  //\n  SearchTopicButton more_button = 2;\n  //\n  repeated ChannelInfo channels = 3;\n}\n\n//\nmessage SearchInfo {\n  //\n  string title = 1;\n  //\n  repeated DynamicItem list = 2;\n  //\n  string track_id = 3;\n  //\n  int64 total = 4;\n  //\n  bool has_more = 5;\n  //\n  string version = 6;\n}\n\n//\nmessage SearchToast {\n  //\n  string desc_text1 = 1;\n  //\n  string desc_text2 = 2;\n}\n\n//\nmessage SearchTopic {\n  //\n  string title = 1;\n  //\n  SearchTopicButton more_button = 2;\n  //\n  repeated SearchTopicItem items = 3;\n}\n\n//\nmessage SearchTopicButton {\n  //\n  string title = 1;\n  //\n  string jump_uri = 2;\n}\n\n//\nmessage SearchTopicItem {\n  //\n  int64 topic_id = 1;\n  //\n  string topic_name = 2;\n  //\n  string desc = 3;\n  //\n  string url = 4;\n  //\n  bool is_activity = 5;\n}\n\n//\nmessage SetDecisionReq {\n  //\n  int32 result = 1;\n  //\n  CampusReqFromType from_type = 2;\n}\n\n//\nmessage SetRecentCampusReq {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  CampusReqFromType from_type = 3;\n}\n\n// 分享渠道组件\nmessage ShareChannel {\n  // 分享名称\n  string name = 1;\n  // 分享按钮图片\n  string image = 2;\n  // 分享渠道\n  string channel = 3;\n  // 预约卡分享图信息，仅分享有预约信息的动态时存在\n  ShareReserve reserve = 4;\n}\n\n// 预约卡分享图信息\nmessage ShareReserve {\n  // 展示标题\n  string title = 1;\n  // 描述(时间+类型)\n  string desc = 2;\n  // 二维码附带icon\n  string qr_code_icon = 3;\n  // 二维码附带文本\n  string qr_code_text = 4;\n  // 二维码url\n  string qr_code_url = 5;\n  //\n  AdditionUserInfo user_info = 6;\n}\n\n//\nenum ShowType {\n  show_type_none = 0;   //\n  show_type_backup = 1; //\n}\n\n// 排序类型\nmessage SortType {\n  // 排序策略\n  // 1:推荐排序 2:最常访问 3:最近关注\n  int32  sort_type = 1;\n  // 排序策略名称\n  string sort_type_name = 2;\n}\n\n//\nmessage StoryArchive {\n  //\n  string cover = 1;\n  //\n  int64 aid = 2;\n  //\n  string uri = 3;\n  //\n  Dimension dimension = 4;\n}\n\n//\nmessage StoryItem {\n  //\n  UserInfo author = 1;\n  //\n  string desc = 2;\n  //\n  int64 status = 3;\n  //\n  int32 type = 4;\n  oneof rcmd_item {\n    //\n    StoryArchive story_archive = 5;\n  }\n}\n\n//\nenum StyleType {\n  STYLE_TYPE_NONE = 0;   //\n  STYLE_TYPE_LIVE = 1;   //\n  STYLE_TYPE_DYN_UP = 2; //\n}\n\n//\nmessage SubscribeCampusReq {\n  //\n  int64 campus_id = 1;\n  //\n  string campus_name = 2;\n  //\n  CampusReqFromType from_type = 3;\n}\n\n//\nmessage TextNode {\n  enum TextNodeType {\n    INVALID = 0;\n    WORDS = 1;\n    EMOTE = 2;\n    AT = 3;\n    BIZ_LINK = 4;\n  }\n  //\n  TextNodeType node_type = 1;\n  string raw_text = 2;\n  //\n  oneof text {\n    WordNode word = 3;\n    EmoteNode emote = 4;\n    LinkNode link = 5;\n  }\n}\n\n//\nmessage TextParagraph {\n  //\n  repeated TextNode nodes = 1;\n}\n\n//\nmessage TextWithPriority {\n  //\n  string text = 1;\n  //\n  int64 priority = 2;\n}\n\n// 免流类型\nenum TFType {\n  TF_UNKNOWN = 0; // 未知\n  U_CARD = 1; // 联通卡\n  U_PKG = 2;  // 联通免流包\n  C_CARD = 3; // 移动卡\n  C_PKG = 4;  // 移动免流包\n  T_CARD = 5; // 电信卡\n  T_PKG = 6;  // 电信免流包\n}\n\n// 三点-关注\nmessage ThreePointAttention {\n  // attention icon\n  string attention_icon = 1;\n  // 关注时显示的文案\n  string attention_text = 2;\n  // not attention icon\n  string not_attention_icon = 3;\n  // 未关注时显示的文案\n  string not_attention_text = 4;\n  // 当前关注状态\n  ThreePointAttentionStatus status = 5;\n}\n\n// 枚举-三点关注状态\nenum ThreePointAttentionStatus {\n  tp_not_attention = 0; //\n  tp_attention = 1;     //\n}\n\n// 三点-自动播放 旧版不维护\nmessage ThreePointAutoPlay {\n  // open icon\n  string open_icon = 1;\n  // 开启时显示文案\n  string open_text = 2;\n  // close icon\n  string close_icon = 3;\n  // 关闭时显示文案\n  string close_text = 4;\n  // 开启时显示文案v2\n  string open_text_v2 = 5;\n  // 关闭时显示文案v2\n  string close_text_v2 = 6;\n  // 仅wifi/免流 icon\n  string only_icon = 7;\n  // 仅wifi/免流 文案\n  string only_text = 8;\n  // open icon v2\n  string open_icon_v2 = 9;\n  // close icon v2\n  string close_icon_v2 = 10;\n}\n\n// 三点-评论\nmessage ThreePointComment {\n  // 精选评论区功能\n  CommentDetail up_selection = 1;\n  // up关闭评论区功能\n  CommentDetail up_close = 2;\n  // icon\n  string icon = 3;\n  // 标题\n  string title = 4;\n}\n\n// 三点-默认结构(使用此背景、举报、删除)\nmessage ThreePointDefault {\n  // icon\n  string icon = 1;\n  // 标题\n  string title = 2;\n  // 跳转链接\n  string uri = 3;\n  // id\n  string id = 4;\n  //\n  ThreePointDefaultToast toast = 5;\n}\n\n//\nmessage ThreePointDefaultToast {\n  //\n  string title = 1;\n  //\n  string desc = 2;\n}\n\n// 三点-不感兴趣\nmessage ThreePointDislike {\n  // icon\n  string icon = 1;\n  // 标题\n  string title = 2;\n}\n\n// 三点-收藏\nmessage ThreePointFavorite {\n  // icon\n  string icon = 1;\n  // 标题\n  string title = 2;\n  // 物料ID\n  int64 id = 3;\n  // 是否订阅\n  bool is_favourite = 4;\n  // 取消收藏图标\n  string cancel_icon = 5;\n  // 取消收藏文案\n  string cancel_title = 6;\n}\n\n//\nmessage ThreePointHide {\n  //\n  string icon = 1;\n  //\n  string title = 2;\n  //\n  ThreePointHideInteractive interactive = 3;\n  //\n  int64 blook_fid = 4;\n  //\n  string blook_type = 5;\n}\n\n//\nmessage ThreePointHideInteractive {\n  //\n  string title = 1;\n  //\n  string confirm = 2;\n  //\n  string cancel = 3;\n  //\n  string toast = 4;\n}\n\n// 三点Item\nmessage ThreePointItem {\n  //类型\n  ThreePointType type = 1;\n  oneof item {\n    // 默认结构\n    ThreePointDefault default = 2;\n    // 自动播放\n    ThreePointAutoPlay auto_player = 3;\n    // 分享\n    ThreePointShare share = 4;\n    // 关注\n    ThreePointAttention attention = 5;\n    // 稍后在看\n    ThreePointWait wait = 6;\n    // 不感兴趣\n    ThreePointDislike dislike = 7;\n    // 收藏\n    ThreePointFavorite favorite = 8;\n    // 置顶\n    ThreePointTop top = 9;\n    // 评论\n    ThreePointComment comment = 10;\n    //\n    ThreePointHide hide = 11;\n    //\n    ThreePointTopicIrrelevant topic_irrelevant = 12;\n  }\n}\n\n// 三点-分享\nmessage ThreePointShare {\n  // icon\n  string icon = 1;\n  // 标题\n  string title = 2;\n  // 分享渠道\n  repeated ThreePointShareChannel channel = 3;\n  // 分享渠道名\n  string channel_name = 4;\n  // 预约卡分享图信息，仅分享有预约信息的动态时存在\n  ShareReserve reserve = 5;\n}\n\n// 三点-分享渠道\nmessage ThreePointShareChannel {\n  // icon\n  string icon = 1;\n  // 名称\n  string title = 2;\n}\n\n// 三点-置顶\nmessage ThreePointTop {\n  // icon\n  string icon = 1;\n  // 标题\n  string title = 2;\n  // 状态\n  TopType type = 3;\n}\n\n//\nmessage ThreePointTopicIrrelevant {\n  //\n  string icon = 1;\n  //\n  string title = 2;\n  //\n  string toast = 3;\n  //\n  int64 topic_id = 4;\n  //\n  int64 res_id = 5;\n  //\n  int64 res_type = 6;\n  //\n  string reason = 7;\n}\n\n// 三点类型\nenum ThreePointType {\n  tp_none = 0;           // 占位\n  background = 1;        // 使用此背景\n  auto_play = 2;         // 自动播放\n  share = 3;             // 分享\n  wait = 4;              // 稍后再播\n  attention = 5;         // 关注\n  report = 6;            // 举报\n  delete = 7;            // 删除\n  dislike = 8;           // 不感兴趣\n  favorite = 9;          // 收藏\n  top = 10;              // 置顶\n  comment = 11;          // 评论\n  hide = 12;             //\n  campus_delete = 13;    //\n  topic_irrelevant = 14; //\n}\n\n// 三点-稍后在看\nmessage ThreePointWait {\n  // addition icon\n  string addition_icon = 1;\n  // 已添加时的文案\n  string addition_text = 2;\n  // no addition icon\n  string no_addition_icon = 3;\n  // 未添加时的文案\n  string no_addition_text = 4;\n  // avid\n  int64 id = 5;\n}\n\n//\nenum ThumbType {\n  cancel = 0; //\n  thumb = 1;  //\n}\n\n// 顶部预约卡\nmessage TopAdditionUP {\n  // 预约卡\n  repeated AdditionUP up = 1;\n  // 折叠数量，大于多少个进行折叠\n  int32 has_fold = 2;\n}\n\n// 话题广场操作按钮\nmessage TopicButton {\n  // 按钮图标\n  string icon = 1;\n  // 按钮文案\n  string title = 2;\n  // 跳转\n  string jump_uri = 3;\n  //\n  bool red_dot = 4;\n}\n\n//\nmessage TopicItem {\n  //\n  int64 topic_id = 1;\n  //\n  string topic_name = 2;\n  //\n  string url = 3;\n  //\n  string desc = 4;\n  //\n  string desc2 = 5;\n  //\n  string rcmd_desc = 6;\n}\n\n// 综合页-话题广场\nmessage TopicList {\n  // 模块标题\n  string title = 1;\n  // 话题列表\n  repeated TopicListItem topic_list_item = 2;\n  // 发起活动\n  TopicButton act_button = 3;\n  // 查看更多\n  TopicButton more_button = 4;\n  // 透传服务端上报\n  string server_info = 5;\n}\n\n// 综合页-话题广场-话题\nmessage TopicListItem {\n  // 前置图标\n  string icon = 1;\n  // 前置图标文案\n  string icon_title = 2;\n  // 话题id\n  int64 topic_id = 3;\n  // 话题名\n  string topic_name = 4;\n  // 跳转链接\n  string url = 5;\n  // 卡片位次\n  int64 pos = 6;\n  // 透传服务端上报\n  string server_info = 7;\n  //\n  string head_icon_url = 8;\n  //\n  int64 up_mid = 9;\n  //\n  string tail_icon_url = 10;\n  //\n  string extension = 11;\n}\n\n//\nmessage TopicListReply {\n  //\n  repeated TopicItem items = 1;\n  //\n  bool has_more = 2;\n  //\n  string offset = 3;\n}\n\n//\nmessage TopicListReq {\n  //\n  int64 campus_id = 1;\n  //\n  string offset = 2;\n}\n\n//\nmessage TopicMergedResource {\n  //\n  int32 merge_type = 1;\n  //\n  int32 merged_res_cnt = 2;\n}\n\n//\nmessage TopicRcmdCard {\n  //\n  int64 topic_id = 1;\n  //\n  string topic_name = 2;\n  //\n  string url = 3;\n  //\n  CampusLabel button = 4;\n  //\n  string desc1 = 5;\n  //\n  string desc2 = 6;\n  //\n  string update_desc = 7;\n}\n\n//\nmessage TopicSquareInfo {\n  //\n  string title = 1;\n  //\n  CampusLabel button = 2;\n  //\n  TopicRcmdCard rcmd = 3;\n}\n\n//\nmessage TopicSquareReply {\n  //\n  TopicSquareInfo info = 1;\n}\n\n//\nmessage TopicSquareReq {\n  //\n  int64 campus_id = 1;\n}\n\n// 状态\nenum TopType {\n  top_none = 0;   // 默认 置顶\n  top_cancel = 1; // 取消置顶\n}\n\n// 综合页-无关注列表\nmessage Unfollow {\n  // 标题展示文案\n  string title = 1;\n  // 无关注列表\n  repeated UnfollowUserItem list = 2;\n  // trackID\n  string TrackId = 3;\n}\n\n//\nmessage UnfollowMatchReq {\n  //\n  int64 cid = 1;\n}\n\n// 综合页-无关注列表\nmessage UnfollowUserItem {\n  // 是否有更新\n  bool has_update = 1;\n  // up主头像\n  string face = 2;\n  // up主昵称\n  string name = 3;\n  // up主uid\n  int64 uid = 4;\n  // 排序字段 从1开始\n  int32 pos = 5;\n  // 直播状态\n  LiveState live_state = 6;\n  // 认证信息\n  OfficialVerify official = 7;\n  // 大会员信息\n  VipInfo vip = 8;\n  // up介绍\n  string sign = 9;\n  // 标签信息\n  string label = 10;\n  // 按钮\n  AdditionalButton button = 11;\n  // 跳转地址\n  string uri = 12;\n}\n\n//\nmessage UpdateTabSettingReq {\n  //\n  HomePageTabSttingStatus status = 1;\n}\n\n// 动态顶部up列表-up主信息\nmessage UpListItem {\n  // 是否有更新\n  bool has_update = 1;\n  // up主头像\n  string face = 2;\n  // up主昵称\n  string name = 3;\n  // up主uid\n  int64 uid = 4;\n  // 排序字段 从1开始\n  int64 pos = 5;\n  // 用户类型\n  UserItemType user_item_type = 6;\n  // 直播头像样式-日\n  UserItemStyle display_style_day = 7;\n  // 直播头像样式-夜\n  UserItemStyle display_style_night = 8;\n  // 直播埋点\n  int64 style_id = 9;\n  // 直播状态\n  LiveState live_state = 10;\n  // 分割线\n  bool separator = 11;\n  // 跳转\n  string uri = 12;\n  // UP主预约上报使用\n  bool is_recall = 13;\n  //\n  IconBadge update_icon = 14;\n  //\n  string live_rcmd_reason = 15;\n  //\n  string live_cover = 16;\n  //\n  string personal_extra = 17;\n}\n\n// 最常访问-查看更多\nmessage UpListMoreLabel {\n  // 文案\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n}\n\n// 用户信息\nmessage UserInfo {\n  // 用户mid\n  int64 mid = 1;\n  // 用户昵称\n  string name = 2;\n  // 用户头像\n  string face = 3;\n  // 认证信息\n  OfficialVerify official = 4;\n  // 大会员信息\n  VipInfo vip = 5;\n  // 直播信息\n  LiveInfo live = 6;\n  // 空间页跳转链接\n  string uri = 7;\n  // 挂件信息\n  UserPendant pendant = 8;\n  // 认证名牌\n  Nameplate nameplate = 9;\n  // 用户等级\n  int32 level = 10;\n  // 用户简介\n  string sign = 11;\n  //\n  int32 face_nft = 12;\n  //\n  int32 face_nft_new = 13;\n  //\n  NFTInfo nft_info = 14;\n  //\n  int32 is_senior_member = 15;\n  //\n  bilibili.dagw.component.avatar.v1.AvatarItem avatar = 16;\n}\n\n// 直播头像样式\nmessage UserItemStyle {\n  //\n  string rect_text = 1;\n  //\n  string rect_text_color = 2;\n  //\n  string rect_icon = 3;\n  //\n  string rect_bg_color = 4;\n  //\n  string outer_animation = 5;\n}\n\n// 用户类型\nenum UserItemType {\n  user_item_type_none = 0;        //\n  user_item_type_live = 1;        //\n  user_item_type_live_custom = 2; //\n  user_item_type_normal = 3;      //\n  user_item_type_extend = 4;      //\n  user_item_type_premiere_reserve = 5;\n  user_item_type_premiere = 6;\n  user_item_type_live_card = 7;\n  user_item_type_ogv_season = 8;\n  user_item_type_ugc_season = 9;\n}\n\n// 头像挂件信息\nmessage UserPendant {\n  // pid\n  int64 pid = 1;\n  // 名称\n  string name = 2;\n  // 图片链接\n  string image = 3;\n  // 有效期\n  int64 expire = 4;\n}\n\n// 角标信息\nmessage VideoBadge {\n  // 文案\n  string text = 1;\n  // 文案颜色-日间\n  string text_color = 2;\n  // 文案颜色-夜间\n  string text_color_night = 3;\n  // 背景颜色-日间\n  string bg_color = 4;\n  // 背景颜色-夜间\n  string bg_color_night = 5;\n  // 边框颜色-日间\n  string border_color = 6;\n  // 边框颜色-夜间\n  string border_color_night = 7;\n  // 样式\n  int32 bg_style = 8;\n  // 背景透明度-日间\n  int32 bg_alpha = 9;\n  // 背景透明度-夜间\n  int32 bg_alpha_night = 10;\n}\n\n// 番剧类型\nenum VideoSubType {\n  VideoSubTypeNone = 0;        // 没有子类型\n  VideoSubTypeBangumi = 1;     // 番剧\n  VideoSubTypeMovie = 2;       // 电影\n  VideoSubTypeDocumentary = 3; // 纪录片\n  VideoSubTypeDomestic = 4;    // 国创\n  VideoSubTypeTeleplay = 5;    // 电视剧\n}\n\n// 视频类型\nenum VideoType {\n  video_type_general = 0;  //普通视频\n  video_type_dynamic = 1;  //动态视频\n  video_type_playback = 2; //直播回放视频\n  video_type_story = 3;    //\n}\n\n// 大会员信息\nmessage VipInfo {\n  // 大会员类型\n  int32 Type = 1;\n  // 大会员状态\n  int32 status = 2;\n  // 到期时间\n  int64 due_date = 3;\n  // 标签\n  VipLabel label = 4;\n  // 主题\n  int32 theme_type = 5;\n  // 大会员角标\n  // 0:无角标 1:粉色大会员角标 2:绿色小会员角标\n  int32 avatar_subscript = 6;\n  // 昵称色值，可能为空，色值示例：#FFFB9E60\n  string nickname_color = 7;\n}\n\n// 大会员标签\nmessage VipLabel {\n  // 图片地址\n  string path = 1;\n  // 文本值\n  string text = 2;\n  // 对应颜色类型\n  string label_theme = 3;\n}\n\n// 状态\nenum VoteStatus {\n  normal = 0;    // 正常\n  anonymous = 1; // 匿名\n}\n\n// 提权样式\nmessage Weight {\n  // 提权展示标题\n  string title = 1;\n  // 下拉框内容\n  repeated WeightItem items = 2;\n  // icon\n  string icon = 3;\n}\n\n// 热门默认跳转按钮\nmessage WeightButton {\n  string jump_url = 1;\n  // 展示文案\n  string title = 2;\n}\n\n// 提权不感兴趣\nmessage WeightDislike {\n  // 负反馈业务类型 作为客户端调用负反馈接口的参数\n  string feed_back_type = 1;\n  // 展示文案\n  string title = 2;\n}\n\n// 提权样式\nmessage WeightItem {\n  // 类型\n  WeightType type = 1;\n  oneof item {\n    // 热门默认跳转按钮\n    WeightButton button = 2;\n    // 提权不感兴趣\n    WeightDislike dislike = 3;\n  }\n}\n\n// 枚举-提权类型\nenum WeightType {\n  weight_none = 0;    // 默认 占位\n  weight_dislike = 1; // 不感兴趣\n  weight_jump = 2;    // 跳链\n}\n\n//\nenum WFItemType {\n  WATER_FLOW_TYPE_NONE = 0;\n  WATER_FLOW_TYPE_ARCHIVE = 1;\n  WATER_FLOW_TYPE_DYNAMIC = 2;\n}\n\n//\nmessage WordNode {\n  //\n  message WordNodeStyle {\n    //\n    bool bold = 1;\n    //\n    bool italic = 2;\n    //\n    bool strikethrough = 3;\n    //\n    bool underline = 4;\n  }\n  //\n  string words = 1;\n  //\n  double font_size = 2;\n  //\n  Colors color = 3;\n  //\n  WordNodeStyle style = 4;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/dynamic/v2/opus.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.dynamic.v2;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\nimport \"bilibili/app/dynamic/v2/dynamic.proto\";\n\nservice Opus {\n  //\n  rpc OpusDetail (OpusDetailReq) returns (OpusDetailResp);\n}\n\n//\nmessage OpusDetailReq {\n  //\n  OpusType opus_type = 1;\n  //\n  int64 oid = 2;\n  //\n  int64 dyn_type = 3;\n  //\n  string share_id = 4;\n  //\n  int32 share_mode = 9;\n  //\n  int32 local_time = 10;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 11;\n  //\n  Config config = 12;\n}\n\n//\nmessage OpusDetailResp {\n  //\n  OpusItem opus_item = 1;\n}\n\n//\nmessage OpusItem {\n  //\n  int64 opus_id = 1;\n  //\n  OpusType opus_type = 2;\n  //\n  int64 oid = 3;\n  //\n  repeated Module modules = 4;\n  //\n  Extend extend = 5;\n}\n\nenum OpusType {\n  OPUS_TYPE_DYN = 0;\n  OPUS_TYPE_ARTICLE = 1;\n  OPUS_TYPE_NOTE = 2;\n  OPUS_TYPE_WORD = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/interfaces/v1/history.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.interface.v1;\n\noption java_package = \"bilibili.app.interfaces.v1\";\noption java_multiple_files = true;\n\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\n\n// 历史记录\nservice History {\n  // 获取历史记录tab\n  rpc HistoryTab (HistoryTabReq) returns (HistoryTabReply);\n  // 获取历史记录列表(旧版)\n  rpc Cursor (CursorReq) returns (CursorReply);\n  // 获取历史记录列表\n  rpc CursorV2 (CursorV2Req) returns (CursorV2Reply);\n  // 删除历史记录\n  rpc Delete (DeleteReq) returns (NoReply);\n  // 搜索历史记录\n  rpc Search (SearchReq) returns (SearchReply);\n  // 清空历史记录\n  rpc Clear (ClearReq) returns (NoReply);\n  // 获取最新的历史记录\n  rpc LatestHistory (LatestHistoryReq) returns (LatestHistoryReply);\n}\n\n// 专栏卡片\nmessage CardArticle {\n  // 封面url\n  repeated string covers = 1;\n  // UP主昵称\n  string name = 2;\n  // UP主mid\n  int64 mid = 3;\n  // 是否展示关注按钮\n  bool displayAttention = 4;\n  // 角标\n  string badge = 5;\n  // 关系信息\n  Relation relation = 6;\n}\n\n// 课程卡片\nmessage CardCheese {\n  // 封面url\n  string cover = 1;\n  // 观看进度\n  int64 progress = 2;\n  // 总计时长\n  int64 duration = 3;\n  // 单集标题\n  string subtitle = 4;\n  //\n  int64 state = 5;\n}\n\n// 直播卡片\nmessage CardLive {\n  // 封面url\n  string cover = 1;\n  // 主播昵称\n  string name = 2;\n  // 主播mid\n  int64 mid = 3;\n  // 直播分区名\n  string tag = 4;\n  // 直播状态\n  int32 ststus = 5;\n  // 是否展示关注按钮\n  bool display_attention = 6;\n  // 关系信息\n  Relation relation = 7;\n}\n\n// pgc稿件卡片\nmessage CardOGV {\n  // 封面url\n  string cover = 1;\n  // 观看进度\n  int64 progress = 2;\n  // 总计时长\n  int64 duration = 3;\n  // 单集标题\n  string subtitle = 4;\n  //\n  string badge = 5;\n  //\n  int64 state = 6;\n}\n\n// ugc稿件卡片\nmessage CardUGC {\n  // 封面url\n  string cover = 1;\n  // 观看进度\n  int64 progress = 2;\n  // 视频长度\n  int64 duration = 3;\n  // UP主昵称\n  string name = 4;\n  // UP主mid\n  int64 mid = 5;\n  // 是否展示关注按钮\n  bool display_attention = 6;\n  // 历史观看视频cid\n  int64 cid = 7;\n  // 历史观看视频分P\n  int32 page = 8;\n  // 历史观看视频分P的标题\n  string subtitle = 9;\n  // 关系信息\n  Relation relation = 10;\n  // 稿件bvid\n  string bvid = 11;\n  // 总分P数\n  int64 videos = 12;\n  // 短链接\n  string short_link = 13;\n  // 分享副标题\n  string share_subtitle = 14;\n  // 播放数\n  int64 view = 15;\n  //\n  int64 state = 16;\n}\n\n// 清空历史记录-请求\nmessage ClearReq {\n  // 业务类型\n  // archive:视频 live:直播 article:专栏 goods:商品 show:展演\n  string business = 1;\n}\n\n// 游标信息\nmessage Cursor {\n  // 本页最大值游标值\n  int64 max = 1;\n  // 本页最大值游标类型\n  int32 maxTp = 2;\n}\n\n// 历史记录卡片信息\nmessage CursorItem {\n  // 主体数据\n  oneof card_item {\n    // ugc稿件\n    CardUGC card_ugc = 1;\n    // pgc稿件\n    CardOGV card_ogv = 2;\n    // 专栏\n    CardArticle card_article = 3;\n    // 直播\n    CardLive card_live = 4;\n    // 课程\n    CardCheese card_cheese = 5;\n  }\n  // 标题\n  string title = 6;\n  // 目标uri/url\n  string uri = 7;\n  // 观看时间\n  int64 viewAt = 8;\n  // 历史记录id\n  int64 kid = 9;\n  // 业务id\n  int64 oid = 10;\n  // 业务类型\n  // archive:视频 live:直播 article:专栏 goods:商品 show:展演\n  string business = 11;\n  // 业务类型代码\n  int32 tp = 12;\n  // 设备标识\n  DeviceType dt = 13;\n  // 是否有分享按钮\n  bool has_share = 14;\n}\n\n// 获取历史记录列表(旧版)-响应\nmessage CursorReply {\n  // 卡片内容\n  repeated CursorItem items = 1;\n  // 顶部tab\n  repeated CursorTab tab = 2;\n  // 游标信息\n  Cursor cursor = 3;\n  // 是否未拉取完\n  bool hasMore = 4;\n}\n\n// 获取历史记录列表(旧版)-请求\nmessage CursorReq {\n  // 游标信息\n  Cursor cursor = 1;\n  // 业务类型\n  // all:全部 archive:视频 live:直播 article:专栏\n  string business = 2;\n  // 秒开参数(旧版)\n  PlayerPreloadParams player_preload = 3;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 4;\n}\n\n// 业务分类表\nmessage CursorTab {\n  // 业务类型\n  string business = 1;\n  // 名称\n  string name = 2;\n  // 路由uri\n  string router = 3;\n  // tab定位\n  bool focus = 4;\n}\n\n// 获取历史记录列表-响应\nmessage CursorV2Reply {\n  // 卡片内容\n  repeated CursorItem items = 1;\n  // 游标信息\n  Cursor cursor = 2;\n  // 是否未拉取完\n  bool hasMore = 3;\n  //\n  string empty_link = 4;\n}\n\n// 获取历史记录列表-请求\nmessage CursorV2Req {\n  // 游标信息\n  Cursor cursor = 1;\n  // 业务类型\n  // archive:视频 live:直播 article:专栏 goods:商品 show:展演\n  string business = 2;\n  // 秒开参数(旧版)\n  PlayerPreloadParams player_preload = 3;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 4;\n  // 是否选择本机的播放历史\n  bool is_local = 5;\n}\n\n// 设备标识代码\nenum DT {\n  Unknown = 0; // 未知\n  Phone = 1; // 手机端\n  Pad = 2; // ipad端\n  PC = 3; // web端\n  TV = 4; // TV端\n  Car = 5; // 车机端\n  Iot = 6; // 物联设备\n  AndPad = 7; // apad端\n}\n\n// 删除历史记录-请求\nmessage DeleteReq {\n  // 历史记录信息\n  HisInfo his_info = 1;\n}\n\n// 设备类型\nmessage DeviceType {\n  // 设备标识代码\n  DT type = 1;\n  // 图标url\n  string icon = 2;\n}\n\n// 历史记录信息\nmessage HisInfo {\n  // 业务类型\n  // archive:视频 live:直播 article:专栏 goods:商品 show:展演\n  string business = 1;\n  // 历史记录id\n  int64 kid = 2;\n}\n\n// 搜索历史记录来源\nenum HistorySource {\n  history_VALUE = 0; // 主站历史记录页\n  shopping_VALUE = 1; // 会员购浏览记录\n}\n\n// 获取历史记录tab-响应\nmessage HistoryTabReply {\n  // tab列表\n  repeated CursorTab tab = 1;\n}\n\n// 获取历史记录tab-请求\nmessage HistoryTabReq {\n  // 业务类型\n  // archive:视频 live:直播 article:专栏 goods:商品 show:展演\n  string business = 1;\n  // 查询请求来源\n  HistorySource source = 2;\n  // 搜索关键词\n  string keyword = 3;\n}\n\n// 获取最新的历史记录-响应\nmessage LatestHistoryReply {\n  // 卡片内容\n  CursorItem items = 1;\n  // 场景\n  string scene = 2;\n  // 弹窗停留时间\n  int64 rtime = 3;\n  // 分组的标志(客户端埋点上报)\n  string flag = 4;\n}\n\n// 获取最新的历史记录-请求\nmessage LatestHistoryReq {\n  // 业务类型\n  // archive:视频 live:直播 article:专栏 goods:商品 show:展演\n  string business = 1;\n  // 秒开参数\n  PlayerPreloadParams player_preload = 2;\n}\n\n// 空响应\nmessage NoReply {\n\n}\n\n// 页面信息\nmessage Page {\n  // 当前页码\n  int64 pn = 1;\n  // 总计条目数\n  int64 total = 2;\n}\n\n// 秒开参数\nmessage PlayerPreloadParams {\n  //清晰度\n  int64 qn = 1;\n  // 流版本\n  int64 fnver = 2;\n  // 流类型\n  int64 fnval = 3;\n  // 是否强制域名\n  int64 forceHost = 4;\n  // 是否4K\n  int64 fourk = 5;\n}\n\n// 关系信息\nmessage Relation {\n  // 关系状态\n  // 1:未关注 2:已关注 3:被关注 4:互关\n  int32 status = 1;\n  // 用户关注UP主\n  int32 is_follow = 2;\n  // UP主关注用户\n  int32 is_followed = 3;\n}\n\n// 搜索历史记录-响应\nmessage SearchReply {\n  // 卡片内容\n  repeated CursorItem items = 1;\n  // 是否未拉取完\n  bool hasMore = 2;\n  // 页面信息\n  Page page = 3;\n}\n\n// 搜索历史记录-请求\nmessage SearchReq {\n  // 关键词\n  string keyword = 1;\n  // 页码\n  int64 pn = 2;\n  // 业务类型\n  // archive:视频 live:直播 article:专栏 goods:商品 show:展演\n  string business = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/interfaces/v1/media.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.interface.v1;\n\noption java_package = \"bilibili.app.interfaces.v1\";\noption java_multiple_files = true;\n\n//\nservice Media {\n  //\n  rpc MediaTab(MediaTabReq) returns (MediaTabReply);\n  //\n  rpc MediaDetail(MediaDetailReq) returns (MediaDetailReply);\n  //\n  rpc MediaVideo(MediaVideoReq) returns (MediaVideoReply);\n  //\n  rpc MediaRelation(MediaRelationReq) returns (MediaRelationReply);\n  //\n  rpc MediaFollow(MediaFollowReq) returns (MediaFollowReply);\n}\n\n//\nmessage BigItem {\n  //\n  string title = 1;\n  //\n  string cover_image_uri = 2;\n  //\n  string uri = 3;\n  //\n  string cover_right_text = 4;\n  //\n  string cover_left_text1 = 5;\n  //\n  int64 cover_left_icon1 = 6;\n  //\n  string cover_left_text2 = 7;\n  //\n  int64 cover_left_icon2 = 8;\n  //\n  UserCard user_card = 9;\n  //\n  LikeButton like_button = 10;\n  //\n  int64 param = 11;\n}\n\n//\nmessage Button {\n  //\n  string title = 1;\n  //\n  string link = 2;\n  //\n  string id = 3;\n  //\n  int64 icon = 4;\n  //\n  ButType but_type = 5;\n  //\n  int32 follow_state = 6;\n  //\n  string has_title = 7;\n}\n\n//\nenum ButType {\n  BUT_INVALID = 0; //\n  BUT_REDIRECT = 1; //\n  BUT_LIKE = 2; //\n}\n\n//\nmessage Cast {\n  //\n  repeated MediaPerson person = 1;\n  //\n  string title = 2;\n}\n\n//\nmessage ChannelInfo {\n  //\n  int64 channel_id = 1;\n  //\n  bool subscribed = 2;\n}\n\n//\nmessage LikeButton {\n  //\n  int64 aid = 1;\n  //\n  int32 count = 2;\n  //\n  bool show_count = 3;\n  //\n  string event = 4;\n  //\n  int32 selected = 5;\n  //\n  string event_v2 = 6;\n  //\n  LikeButtonResource like_resource = 7;\n  //\n  LikeButtonResource dis_like_resource = 8;\n  //\n  LikeButtonResource like_night_resource = 9;\n  //\n  LikeButtonResource dis_like_night_resource = 10;\n}\n\n//\nmessage LikeButtonResource {\n  //\n  string url = 1;\n  //\n  string hash = 2;\n}\n\n//\nmessage LikeCard {\n  //\n  int64 like = 1;\n  //\n  bool is_follow = 2;\n}\n\n//\nmessage MediaCard {\n  //\n  string cover = 1;\n  //\n  string cur_title = 2;\n  //\n  string style = 3;\n  //\n  string label = 4;\n  //\n  Button but_first = 5;\n}\n\n//\nmessage MediaDetailReply {\n  //\n  Cast cast = 1;\n  //\n  Staff staff = 2;\n  //\n  Overview overview = 3;\n}\n\n//\nmessage MediaDetailReq {\n  //\n  int64 biz_id = 1;\n  //\n  int64 biz_type = 2;\n}\n\n//\nmessage MediaFollowReply {\n\n}\n\n//\nmessage MediaFollowReq {\n  //\n  string id = 1;\n  //\n  int32 type = 2;\n}\n\n//\nmessage MediaPerson {\n  //\n  string real_name = 1;\n  //\n  string square_url = 2;\n  //\n  string character = 3;\n  //\n  int64 person_id = 4;\n  //\n  string type = 5;\n}\n\n//\nmessage MediaRelationReply {\n  //\n  string offset = 1;\n  //\n  bool has_more = 2;\n  //\n  repeated SmallItem list = 3;\n}\n\n//\nmessage MediaRelationReq {\n  //\n  int64 biz_id = 1;\n  //\n  int64 biz_type = 2;\n  //\n  int64 feed_id = 3;\n  //\n  string offset = 5;\n  //\n  int32 ps = 6;\n}\n\n//\nmessage MediaTabReply {\n  //\n  MediaCard media_card = 1;\n  //\n  repeated ShowTab tab = 2;\n  //\n  int64 default_tab_index = 3;\n  //\n  ChannelInfo channel_info = 4;\n}\n\n//\nmessage MediaTabReq {\n  //\n  int64 biz_id = 1;\n  //\n  int64 biz_type = 2;\n  //\n  string source = 3;\n  //\n  string spmid = 4;\n  //\n  map<string, string> args = 5;\n}\n\n//\nmessage MediaVideoReply {\n  //\n  string offset = 1;\n  //\n  bool has_more = 2;\n  //\n  repeated BigItem list = 3;\n}\n\n//\nmessage MediaVideoReq {\n  //\n  int64 biz_id = 1;\n  //\n  int64 biz_type = 2;\n  //\n  int64 feed_id = 3;\n  //\n  string offset = 5;\n  //\n  int32 ps = 6;\n}\n\n//\nmessage Overview {\n  //\n  string title = 1;\n  //\n  string text = 2;\n}\n\n//\nmessage ShowTab {\n  //\n  TabType tab_type = 1;\n  //\n  string title = 2;\n  //\n  string url = 3;\n}\n\n//\nmessage SmallItem {\n  //\n  string title = 1;\n  //\n  string cover_image_uri = 2;\n  //\n  string uri = 3;\n  //\n  string cover_right_text = 4;\n  //\n  string cover_left_text1 = 5;\n  //\n  int64 cover_left_icon1 = 6;\n  //\n  string cover_left_text2 = 7;\n  //\n  int64 cover_left_icon2 = 8;\n  //\n  int64 param = 9;\n  //\n  int64 mid = 10;\n}\n\n//\nmessage Staff {\n  //\n  string title = 1;\n  //\n  string text = 2;\n}\n\n//\nenum TabType {\n  TAB_INVALID = 0; //\n  TAB_OGV_DETAIL = 6; //\n  TAB_OGV_REPLY = 7; //\n  TAB_FEED_BID = 8; //\n  TAB_FEED_SMALL = 9; //\n}\n\n//\nmessage UserCard {\n  //\n  string user_name = 1;\n  //\n  string user_face = 2;\n  //\n  string user_url = 3;\n  //\n  int64 mid = 4;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/interfaces/v1/search.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.interface.v1;\n\noption java_package = \"bilibili.app.interfaces.v1\";\noption java_multiple_files = true;\n\n// 搜索\nservice Search {\n  // 获取搜索建议\n  rpc Suggest3 (SuggestionResult3Req) returns (SuggestionResult3Reply);\n  //\n  rpc DefaultWords(DefaultWordsReq) returns (DefaultWordsReply);\n}\n\n//\nservice SearchTest {\n  //\n  rpc NotExist(SuggestionResult3Req) returns (SuggestionResult3Reply);\n}\n\n//\nmessage DefaultWordsReply {\n  //\n  string trackid = 1;\n  //\n  string param = 2;\n  //\n  string show = 3;\n  //\n  string word = 4;\n  //\n  int64 show_front = 5;\n  //\n  string exp_str = 6;\n  //\n  string goto = 7;\n  //\n  string value = 8;\n  //\n  string uri = 9;\n}\n\n//\nmessage NftFaceIcon {\n  //\n  int32 region_type = 1;\n  //\n  string icon = 2;\n  //\n  int32 show_status = 3;\n}\n\n//\nmessage DefaultWordsReq {\n  //\n  int64 from = 1;\n  //\n  int64 login_event = 2;\n  //\n  int32 teenagers_mode = 3;\n  //\n  int32 lessons_mode = 4;\n  //\n  string tab = 5;\n  //\n  string event_id = 6;\n  //\n  string avid = 7;\n  //\n  string query = 8;\n  //\n  int64 an = 9;\n  //\n  int64 is_fresh = 10;\n}\n\n// 获取搜索建议-响应\nmessage SuggestionResult3Reply {\n  // 搜索追踪id\n  string trackid = 1;\n  // 搜索建议条目列表\n  repeated ResultItem list = 2;\n  // 搜索的abtest 实验信息\n  string exp_str = 3;\n}\n\n// 获取搜索建议-请求\nmessage SuggestionResult3Req {\n  // 关键字\n  string keyword = 1;\n  // 是否语法高亮\n  // 0:不显示 1:显示\n  int32 highlight = 2;\n  // 是否青少年模式\n  // 1:开启青少年模式\n  int32 teenagers_mode = 3;\n}\n\n// 搜索建议条目\nmessage ResultItem {\n  // 来源\n  string from = 1;\n  // 显示结果(语法高亮)\n  string title = 2;\n  // 搜索关键字\n  string keyword = 3;\n  // 序号\n  int32 position = 4;\n  // 图片\n  string cover = 5;\n  // 图片尺寸\n  double cover_size = 6;\n  // sug词类型\n  string sug_type = 7;\n  // 词条大类型\n  int32 term_type = 8;\n  // 跳转类型\n  string goto = 9;\n  // 跳转uri\n  string uri = 10;\n  // 认证信息\n  OfficialVerify official_verify = 11;\n  // 跳转参数\n  string param = 12;\n  // up主mid\n  int64 mid = 13;\n  // 粉丝数\n  int32 fans = 14;\n  // up主等级\n  int32 level = 15;\n  // up主稿件数\n  int32 archives = 16;\n  // 投稿时间\n  int64 ptime = 17;\n  // season类型名称\n  string season_type_name = 18;\n  // 地区\n  string area = 19;\n  // 作品风格\n  string style = 20;\n  // 描述信息\n  string label = 21;\n  // 评分\n  double rating = 22;\n  // 投票数\n  int32 vote = 23;\n  // 角标\n  repeated ReasonStyle badges = 24;\n  //\n  string styles = 25;\n  //\n  int64 module_id = 26;\n  //\n  string live_link = 27;\n  //\n  int32 face_nft_new = 28;\n  //\n  NftFaceIcon nft_face_icon = 29;\n}\n\n// 认证信息\nmessage OfficialVerify {\n  // 认证类型\n  // 127:未认证 0:个人 1:机构\n  int32 type = 1;\n  // 认证描述\n  string desc = 2;\n}\n\n// 角标\nmessage ReasonStyle {\n  // 角标文案\n  string text = 1;\n  // 文案日间色值\n  string text_color = 2;\n  // 文案夜间色值\n  string text_color_night = 3;\n  // 背景日间色值\n  string bg_color = 4;\n  // 背景夜间色值\n  string bg_color_night = 5;\n  // 边框日间色值\n  string border_color = 6;\n  // 边框夜间色值\n  string border_color_night = 7;\n  // 角标样式\n  // 1:填充模式 2:镂空模式\n  int32 bg_style = 8;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/interfaces/v1/space.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.interface.v1;\n\noption java_package = \"bilibili.app.interfaces.v1\";\noption java_multiple_files = true;\n\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\nimport \"bilibili/app/archive/v1/archive.proto\";\nimport \"bilibili/app/dynamic/v2/dynamic.proto\";\n\n//\nservice Space {\n  //\n  rpc SearchTab(SearchTabReq) returns (SearchTabReply);\n  //\n  rpc SearchArchive(SearchArchiveReq) returns (SearchArchiveReply);\n  //\n  rpc SearchDynamic(SearchDynamicReq) returns (SearchDynamicReply);\n}\n\n//\nmessage Arc {\n  //\n  bilibili.app.archive.v1.Arc archive = 1;\n  //\n  string uri = 2;\n}\n\n//\nmessage Dynamic {\n  //\n  bilibili.app.dynamic.v2.DynamicItem dynamic = 1;\n}\n\nenum From {\n  ArchiveTab = 0; //\n  DynamicTab = 1; //\n}\n\n//\nmessage SearchTabReply {\n  //\n  int64 focus = 1;\n  //\n  repeated Tab tabs = 2;\n}\n\n//\nmessage SearchTabReq {\n  //\n  string keyword = 1;\n  //\n  int64 mid = 2;\n  //\n  int32 from = 3;\n}\n\n//\nmessage SearchArchiveReply {\n  //\n  repeated Arc archives = 1;\n  //\n  int64 total = 2;\n}\n\n//\nmessage SearchArchiveReq {\n  //\n  string keyword = 1;\n  //\n  int64 mid = 2;\n  //\n  int64 pn = 3;\n  //\n  int64 ps = 4;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 5;\n}\n\n//\nmessage SearchDynamicReply {\n  //\n  repeated Dynamic dynamics = 1;\n  //\n  int64 total = 2;\n}\n\n//\nmessage SearchDynamicReq {\n  //\n  string keyword = 1;\n  //\n  int64 mid = 2;\n  //\n  int64 pn = 3;\n  //\n  int64 ps = 4;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 5;\n}\n\n//\nmessage Tab {\n  //\n  string title = 1;\n  //\n  string uri = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/listener/v1/listener.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.listener.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\nimport \"bilibili/app/interfaces/v1/history.proto\";\nimport \"bilibili/app/playurl/v1/playurl.proto\";\n\n// 听视频\nservice Listener {\n  //\n  rpc Ping (google.protobuf.Empty) returns (google.protobuf.Empty);\n  // 获取音频URL\n  rpc PlayUrl (PlayURLReq) returns (PlayURLResp);\n  //\n  rpc BkarcDetails (BKArcDetailsReq) returns (BKArcDetailsResp);\n  //\n  rpc Playlist (PlaylistReq) returns (PlaylistResp);\n  //\n  rpc PlaylistAdd (PlaylistAddReq) returns (google.protobuf.Empty);\n  //\n  rpc PlaylistDel (PlaylistDelReq) returns (google.protobuf.Empty);\n  // 推荐列表\n  rpc RcmdPlaylist (RcmdPlaylistReq) returns (RcmdPlaylistResp);\n  //\n  rpc PlayHistory (PlayHistoryReq) returns (PlayHistoryResp);\n  // 添加历史记录\n  rpc PlayHistoryAdd (PlayHistoryAddReq) returns (google.protobuf.Empty);\n  //\n  rpc PlayHistoryDel (PlayHistoryDelReq) returns (google.protobuf.Empty);\n  // 播放上报\n  rpc PlayActionReport (PlayActionReportReq) returns (google.protobuf.Empty);\n  // 三联\n  rpc TripleLike (TripleLikeReq) returns (TripleLikeResp);\n  // 点赞\n  rpc ThumbUp (ThumbUpReq) returns (ThumbUpResp);\n  // 投币\n  rpc CoinAdd (CoinAddReq) returns (CoinAddResp);\n  //\n  rpc FavItemAdd (FavItemAddReq) returns (FavItemAddResp);\n  //\n  rpc FavItemDel (FavItemDelReq) returns (FavItemDelResp);\n  // 批量处理收藏\n  rpc FavItemBatch (FavItemBatchReq) returns (FavItemBatchResp);\n  //\n  rpc FavoredInAnyFolders (FavoredInAnyFoldersReq) returns (FavoredInAnyFoldersResp);\n  // 用户收藏夹列表\n  rpc FavFolderList (FavFolderListReq) returns (FavFolderListResp);\n  // 收藏夹详细信息\n  rpc FavFolderDetail (FavFolderDetailReq) returns (FavFolderDetailResp);\n  // 创建收藏夹\n  rpc FavFolderCreate (FavFolderCreateReq) returns (FavFolderCreateResp);\n  //\n  rpc FavFolderDelete (FavFolderDeleteReq) returns (FavFolderDeleteResp);\n  // 每日播单列表\n  rpc PickFeed (PickFeedReq) returns (PickFeedResp);\n  // 每日播单详情\n  rpc PickCardDetail (PickCardDetailReq) returns (PickCardDetailResp);\n  //\n  rpc Medialist(MedialistReq) returns (MedialistResp);\n  //\n  rpc Event(EventReq) returns (EventResp);\n}\n\n//\nservice Music {\n  //\n  rpc FavTabShow(FavTabShowReq) returns (FavTabShowResp);\n  //\n  rpc MainFavMusicSubTabList(MainFavMusicSubTabListReq) returns (MainFavMusicSubTabListResp);\n  //\n  rpc MainFavMusicMenuList(MainFavMusicMenuListReq) returns (MainFavMusicMenuListResp);\n  //\n  rpc MenuEdit(MenuEditReq) returns (MenuEditResp);\n  //\n  rpc MenuDelete(MenuDeleteReq) returns (MenuDeleteResp);\n  //\n  rpc MenuSubscribe(MenuSubscribeReq) returns (MenuSubscribeResp);\n  //\n  rpc Click(ClickReq) returns (ClickResp);\n}\n\n//\nmessage Author {\n  //\n  int64 mid = 1;\n  //\n  string name = 2;\n  //\n  string avatar = 3;\n  //\n  FollowRelation relation = 4;\n}\n\n//\nmessage BKArcDetailsReq {\n  //\n  repeated PlayItem items = 1;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs playerArgs = 2;\n}\n\n//\nmessage BKArcDetailsResp {\n  //\n  repeated DetailItem list = 1;\n}\n\n//\nmessage BKArchive {\n  //\n  int64 oid = 1;\n  //\n  string title = 2;\n  //\n  string cover = 3;\n  //\n  string desc = 4;\n  //\n  int64 duration = 5;\n  //\n  int32 rid = 6;\n  //\n  string rname = 7;\n  //\n  int64 publish = 8;\n  //\n  string displayed_oid = 9;\n  //\n  int32 copyright = 10;\n  //\n  BKArcRights rights = 11;\n}\n\n//\nmessage BKArcPart {\n  //\n  int64 oid = 1;\n  //\n  int64 sub_id = 2;\n  //\n  string title = 3;\n  //\n  int64 duration = 4;\n  //\n  int32 page = 5;\n}\n\n//\nmessage BKArcRights {\n  //\n  int32 no_reprint = 1;\n}\n\n//\nmessage BKStat {\n  //\n  int32 like = 1;\n  //\n  int32 coin = 2;\n  //\n  int32 favourite = 3;\n  //\n  int32 reply = 4;\n  //\n  int32 share = 5;\n  //\n  int64 view = 6;\n  //\n  bool has_like = 7;\n  //\n  bool has_coin = 8;\n  //\n  bool has_fav = 9;\n}\n\n//\nmessage CardModule {\n  //\n  int32 module_type = 1;\n  oneof module {\n    //\n    PkcmHeader module_header = 2;\n    //\n    PkcmArchive module_archive = 3;\n    //\n    PkcmCenterButton module_cbtn = 4;\n  }\n}\n\n//\nenum CardModuleType {\n  Module_invalid = 0;\n  Module_header = 1;\n  Module_archive = 2;\n  Module_cbtn = 3;\n}\n\n//\nmessage ClickReq {\n  //\n  int64 sid = 1;\n  //\n  int32 action = 2;\n}\n\n//\nmessage ClickResp {\n\n}\n\n//\nmessage CoinAddReq {\n  //\n  PlayItem item = 1;\n  //\n  int32 num = 2;\n  //\n  bool thumb_up = 3;\n}\n\n//\nmessage CoinAddResp {\n  //\n  string message = 1;\n}\n\n//\nmessage DashItem {\n  //\n  int32 id = 1;\n  //\n  string base_url = 2;\n  //\n  repeated string backup_url = 3;\n  //\n  int32 bandwidth = 4;\n  //\n  string mime_type = 5;\n  //\n  string codecs = 6;\n  //\n  DashSegmentBase segment_base = 12;\n  //\n  int32 codecid = 13;\n  //\n  string md5 = 14;\n  //\n  int64 size = 15;\n}\n\n//\nmessage DashSegmentBase {\n  //\n  string initialization = 1;\n  //\n  string index_range = 2;\n}\n\n//\nmessage DetailItem {\n  //\n  PlayItem item = 1;\n  //\n  BKArchive arc = 2;\n  //\n  repeated BKArcPart parts = 3;\n  //\n  Author owner = 4;\n  //\n  BKStat stat = 5;\n  //\n  int64 last_part = 6;\n  //\n  int64 progress = 7;\n  //\n  int32 playable = 8;\n  //\n  string message = 9;\n  //\n  map<int64, PlayInfo> player_info = 10;\n  //\n  PlayItem associated_item = 11;\n  //\n  int64 last_play_time = 12;\n  //\n  string history_tag = 13;\n  //\n  bilibili.app.interface.v1.DeviceType device_type = 14;\n  //\n  FavFolder ugc_season_info = 15;\n}\n\n//\nmessage EventReq {\n  //\n  int32 event_type = 1;\n  //\n  PlayItem item = 2;\n}\n\n//\nmessage EventResp {\n\n}\n\n//\nmessage EventTracking {\n  //\n  string operator = 1;\n  //\n  string batch = 2;\n  //\n  string track_id = 3;\n  //\n  string entity_type = 4;\n  //\n  string entity_id = 5;\n  //\n  string track_json = 6;\n}\n\n//\nmessage FavFolder {\n  //\n  int64 fid = 1;\n  //\n  int32 folder_type = 2;\n  //\n  FavFolderAuthor owner = 3;\n  //\n  string name = 4;\n  //\n  string cover = 5;\n  //\n  string desc = 6;\n  //\n  int32 count = 7;\n  //\n  int32 attr = 8;\n  //\n  int32 state = 9;\n  //\n  int32 favored = 10;\n  //\n  int64 ctime = 11;\n  //\n  int64 mtime = 12;\n  //\n  int32 stat_fav_cnt = 13;\n  //\n  int32 stat_share_cnt = 14;\n  //\n  int32 stat_like_cnt = 15;\n  //\n  int32 stat_Play_cnt = 16;\n  //\n  int32 stat_reply_cnt = 17;\n  //\n  int32 fav_state = 18;\n}\n\n//\nmessage FavFolderAction {\n  //\n  int64 fid = 1;\n  //\n  int32 folder_type = 2;\n  //\n  int32 action = 3;\n}\n\n//\nmessage FavFolderAuthor {\n  //\n  int64 mid = 1;\n  //\n  string name = 2;\n}\n\n//\nmessage FavFolderCreateReq {\n  //\n  string name = 1;\n  //\n  string desc = 2;\n  //\n  int32 public = 3;\n  //\n  int32 folder_type = 4;\n}\n\n//\nmessage FavFolderCreateResp {\n  //\n  int64 fid = 1;\n  //\n  int32 folder_type = 2;\n  //\n  string message = 3;\n}\n\n//\nmessage FavFolderDeleteReq {\n  //\n  int64 fid = 1;\n  //\n  int32 folder_type = 2;\n}\n\n//\nmessage FavFolderDeleteResp {\n  //\n  string message = 1;\n}\n\n//\nmessage FavFolderDetailReq {\n  //\n  int64 fid = 1;\n  //\n  int32 folder_type = 2;\n  //\n  int64 fav_mid = 3;\n  //\n  FavItem last_item = 4;\n  //\n  int32 page_size = 5;\n  //\n  bool need_folder_info = 6;\n}\n\n//\nmessage FavFolderDetailResp {\n  //\n  int32 total = 1;\n  //\n  bool reach_end = 2;\n  //\n  repeated FavItemDetail list = 3;\n  //\n  FavFolder folder_info = 4;\n}\n\n//\nmessage FavFolderListReq {\n  //\n  repeated int32 folder_types = 1;\n  //\n  PlayItem item = 2;\n}\n\n//\nmessage FavFolderListResp {\n  //\n  repeated FavFolder list = 1;\n}\n\n//\nmessage FavFolderMeta {\n  //\n  int64 fid = 1;\n  //\n  int32 folder_type = 2;\n}\n\n//\nmessage FavItem {\n  //\n  int32 item_type = 1;\n  //\n  int64 oid = 2;\n  //\n  int64 fid = 3;\n  //\n  int64 mid = 4;\n  //\n  int64 mtime = 5;\n  //\n  int64 ctime = 6;\n  //\n  EventTracking et = 7;\n}\n\n//\nmessage FavItemAddReq {\n  //\n  int64 fid = 1;\n  //\n  int32 folder_type = 2;\n  oneof item {\n    //\n    PlayItem play = 3;\n    //\n    FavItem fav = 4;\n  }\n}\n\n//\nmessage FavItemAddResp {\n  //\n  string message = 1;\n}\n\n//\nmessage FavItemAuthor {\n  //\n  int64 mid = 1;\n  //\n  string name = 2;\n}\n\n//\nmessage FavItemBatchReq {\n  //\n  repeated FavFolderAction actions = 1;\n  oneof item {\n    //\n    PlayItem play = 2;\n    //\n    FavItem fav = 3;\n  }\n}\n\n//\nmessage FavItemBatchResp {\n  //\n  string message = 1;\n}\n\n//\nmessage FavItemDelReq {\n  //\n  int64 fid = 1;\n  //\n  int32 folder_type = 2;\n  oneof item {\n    //\n    PlayItem play = 3;\n    //\n    FavItem fav = 4;\n  }\n}\n\n//\nmessage FavItemDelResp {\n  //\n  string message = 1;\n}\n\n//\nmessage FavItemDetail {\n  //\n  FavItem item = 1;\n  //\n  FavItemAuthor owner = 2;\n  //\n  FavItemStat stat = 3;\n  //\n  string cover = 4;\n  //\n  string name = 5;\n  //\n  int64 duration = 6;\n  //\n  int32 state = 7;\n  //\n  string message = 8;\n  //\n  int32 parts = 9;\n}\n\n//\nmessage FavItemStat {\n  //\n  int64 view = 1;\n  //\n  int32 reply = 2;\n}\n\n//\nmessage FavoredInAnyFoldersReq {\n  //\n  repeated int32 folder_types = 1;\n  //\n  PlayItem item = 2;\n}\n\n//\nmessage FavoredInAnyFoldersResp {\n  //\n  repeated FavFolderMeta folders = 1;\n  //\n  PlayItem item = 2;\n}\n\n//\nmessage FavTabShowReq {\n  //\n  int64 mid = 1;\n}\n\n//\nmessage FavTabShowResp {\n  //\n  bool show_menu = 1;\n}\n\n//\nmessage FollowRelation {\n  //\n  int32 status = 1;\n}\n\n//\nmessage FormatDescription {\n  //\n  int32 quality = 1;\n  //\n  string format = 2;\n  //\n  string description = 3;\n  //\n  string display_desc = 4;\n  //\n  string superscript = 5;\n}\n\n//\nenum ListOrder {\n  NO_ORDER = 0;      //\n  ORDER_NORMAL = 1;  //\n  ORDER_REVERSE = 2; //\n  ORDER_RANDOM = 3;  //\n}\n\n//\nenum ListSortField {\n  NO_SORT = 0;      //\n  SORT_CTIME = 1;   //\n  SORT_VIEWCNT = 2; //\n  SORT_FAVCNT = 3;  //\n}\n\n//\nmessage MainFavMusicMenuListReq {\n  //\n  int32 tab_type = 1;\n  //\n  string offset = 2;\n}\n\n//\nmessage MainFavMusicMenuListResp {\n  //\n  int32 tab_type = 1;\n  //\n  repeated MusicMenu menu_list = 2;\n  //\n  bool has_more = 3;\n  //\n  string offset = 4;\n}\n\n//\nmessage MainFavMusicSubTabListReq {\n\n}\n\n//\nmessage MainFavMusicSubTabListResp {\n  //\n  repeated MusicSubTab tabs = 1;\n  //\n  MainFavMusicMenuListResp default_tab_res = 2;\n  //\n  map<int32, MainFavMusicMenuListResp> first_page_res = 3;\n}\n\n//\nmessage MedialistItem {\n  //\n  PlayItem item = 1;\n  //\n  string title = 2;\n  //\n  string cover = 3;\n  //\n  int64 duration = 4;\n  //\n  int32 parts = 5;\n  //\n  int64 up_mid = 6;\n  //\n  string up_name = 7;\n  //\n  int32 state = 8;\n  //\n  string message = 9;\n  //\n  int64 stat_view = 10;\n  //\n  int64 stat_reply = 11;\n}\n\n//\nmessage MedialistReq {\n  //\n  int64 list_type = 1;\n  //\n  int64 biz_id = 2;\n  //\n  string offset = 3;\n}\n\n//\nmessage MedialistResp {\n  //\n  int64 total = 1;\n  //\n  bool has_more = 2;\n  //\n  string offset = 3;\n  //\n  repeated MedialistItem items = 4;\n  //\n  MedialistUpInfo up_info = 5;\n}\n\n//\nmessage MedialistUpInfo {\n  //\n  int64 mid = 1;\n  //\n  string avatar = 2;\n  //\n  int64 fans = 3;\n  //\n  string name = 4;\n}\n\n//\nmessage MenuDeleteReq {\n  //\n  int64 id = 1;\n}\n\n//\nmessage MenuDeleteResp {\n  //\n  string message = 1;\n}\n\n//\nmessage MenuEditReq {\n  //\n  int64 id = 1;\n  //\n  string title = 2;\n  //\n  string desc = 3;\n  //\n  int32 is_public = 4;\n}\n\n//\nmessage MenuEditResp {\n  //\n  string message = 1;\n}\n\n//\nmessage MenuSubscribeReq {\n  //\n  int32 action = 1;\n  //\n  int64 target_id = 2;\n}\n\n//\nmessage MenuSubscribeResp {\n  //\n  string message = 1;\n}\n\n//\nmessage MusicMenu {\n  //\n  int64 id = 1;\n  //\n  int32 menu_type = 2;\n  //\n  string title = 3;\n  //\n  string desc = 4;\n  //\n  string cover = 5;\n  //\n  MusicMenuAuthor owner = 6;\n  //\n  int32 state = 7;\n  //\n  int64 attr = 8;\n  //\n  MusicMenuStat stat = 9;\n  //\n  int64 total = 10;\n  //\n  int64 ctime = 11;\n  //\n  string uri = 12;\n}\n\n//\nmessage MusicMenuAuthor {\n  //\n  int64 mid = 1;\n  //\n  string name = 2;\n  //\n  string avatar = 3;\n}\n\n//\nmessage MusicMenuStat {\n  //\n  int64 play = 1;\n  //\n  int64 reply = 2;\n}\n\n//\nmessage MusicSubTab {\n  //\n  string name = 1;\n  //\n  int32 tab_type = 2;\n  //\n  int64 total = 3;\n}\n\n//\nmessage PageOption {\n  //\n  int32 page_size = 1;\n  //\n  int32 direction = 2;\n  //\n  PlayItem last_item = 3;\n}\n\n//\nmessage PickArchive {\n  //\n  PlayItem item = 1;\n  //\n  string title = 2;\n  //\n  PickArchiveAuthor owner = 3;\n  //\n  string cover = 4;\n  //\n  int64 duration = 5;\n  //\n  int32 parts = 6;\n  //\n  int32 stat_view = 7;\n  //\n  int32 stat_reply = 8;\n  //\n  int32 state = 9;\n  //\n  string message = 10;\n}\n\n//\nmessage PickArchiveAuthor {\n  //\n  int64 mid = 1;\n  //\n  string name = 2;\n}\n\n//\nmessage PickCard {\n  //\n  int64 pick_id = 1;\n  //\n  int64 card_id = 2;\n  //\n  string card_name = 3;\n  //\n  repeated CardModule modules = 4;\n}\n\n//\nmessage PickCardDetailReq {\n  //\n  int64 card_id = 1;\n  //\n  int64 pick_id = 2;\n}\n\n//\nmessage PickCardDetailResp {\n  //\n  int64 card_id = 1;\n  //\n  int64 pick_id = 2;\n  //\n  repeated CardModule modules = 3;\n}\n\n//\nmessage PickFeedReq {\n  //\n  int64 offset = 1;\n}\n\n//\nmessage PickFeedResp {\n  //\n  int64 offset = 1;\n  //\n  repeated PickCard cards = 2;\n}\n\n//\nmessage PkcmArchive {\n  //\n  PickArchive arc = 1;\n  //\n  string pick_reason = 2;\n}\n\n//\nmessage PkcmCenterButton {\n  //\n  string icon_head = 1;\n  //\n  string icon_tail = 2;\n  //\n  string title = 3;\n  //\n  string uri = 4;\n}\n\n//\nmessage PkcmHeader {\n  //\n  string title = 1;\n  //\n  string desc = 2;\n  //\n  string btn_icon = 3;\n  //\n  string btn_text = 4;\n  //\n  string btn_uri = 5;\n}\n\n//\nmessage PlayActionReportReq {\n  //\n  PlayItem item = 1;\n  //\n  string from_spmid = 2;\n}\n\n//\nmessage PlayDASH {\n  //\n  int32 duration = 1;\n  //\n  float min_buffer_time = 2;\n  //\n  repeated DashItem audio = 3;\n}\n\n//\nmessage PlayHistoryAddReq {\n  //\n  PlayItem item = 1;\n  //\n  int64 progress = 2;\n  //\n  int64 duration = 3;\n  //\n  int32 play_style = 4;\n}\n\n//\nmessage PlayHistoryDelReq {\n  //\n  repeated PlayItem items = 1;\n  //\n  bool truncate = 2;\n}\n\n//\nmessage PlayHistoryReq {\n  //\n  PageOption page_opt = 1;\n  //\n  int64 local_today_zero = 2;\n}\n\n//\nmessage PlayHistoryResp {\n  //\n  int32 total = 1;\n  //\n  bool reach_end = 2;\n  //\n  repeated DetailItem list = 3;\n}\n\n//\nmessage PlayInfo {\n  //\n  int32 qn = 1;\n  //\n  string format = 2;\n  //\n  int32 qn_type = 3;\n  oneof info {\n    //\n    PlayURL play_url = 4;\n    //\n    PlayDASH play_dash = 5;\n  }\n  int32 fnver = 6;\n  //\n  int32 fnval = 7;\n  //\n  repeated int32 formats = 8;\n  //\n  int32 video_codecid = 9;\n  //\n  int64 length = 10;\n  //\n  int32 code = 11;\n  //\n  string message = 12;\n  //\n  int64 expire_time = 13;\n  //\n  bilibili.app.playurl.v1.VolumeInfo volume = 14;\n}\n\n//\nmessage PlayItem {\n  //\n  int32 item_type = 1;\n  //\n  int64 oid = 3;\n  //\n  repeated int64 sub_id = 4;\n  //\n  EventTracking et = 5;\n}\n\n//\nmessage PlaylistAddReq {\n  //\n  repeated PlayItem items = 1;\n  oneof pos {\n    //\n    PlayItem after = 2;\n    //\n    bool head = 3;\n    //\n    bool tail = 4;\n  }\n}\n\n//\nmessage PlaylistDelReq {\n  //\n  repeated PlayItem items = 1;\n  //\n  bool truncate = 2;\n}\n\n//\nmessage PlaylistReq {\n  //\n  int32 from = 1;\n  //\n  int64 id = 2;\n  //\n  PlayItem anchor = 3;\n  //\n  PageOption page_opt = 4;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 5;\n  //\n  int64 extra_id = 6;\n  //\n  SortOption sort_opt = 7;\n}\n\n//\nmessage PlaylistResp {\n  //\n  int32 total = 1;\n  //\n  bool reach_start = 2;\n  //\n  bool reach_end = 3;\n  //\n  repeated DetailItem list = 4;\n  //\n  PlayItem last_play = 5;\n  //\n  int64 last_progress = 6;\n}\n\n//\nenum PlaylistSource {\n  DEFAULT = 0;           //\n  MEM_SPACE = 1;         //\n  AUDIO_COLLECTION = 2;  //\n  AUDIO_CARD = 3;        //\n  USER_FAVOURITE = 4;    //\n  UP_ARCHIVE = 5;        //\n  AUDIO_CACHE = 6;       //\n  PICK_CARD = 7;         //\n  MEDIA_LIST = 8;        //\n}\n\n//\nmessage PlayURL {\n  //\n  repeated ResponseUrl durl = 1;\n}\n\n//\nmessage PlayURLReq {\n  //\n  PlayItem item = 1;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 2;\n}\n\n//\nmessage PlayURLResp {\n  //\n  PlayItem item = 1;\n  //\n  int32 playable = 2;\n  //\n  string message = 3;\n  //\n  map<int64, PlayInfo> playerInfo = 4;\n}\n\n//\nmessage RcmdPlaylistReq {\n  //\n  int32 from = 1;\n  //\n  int64 id = 2;\n  //\n  bool need_history = 3;\n  //\n  bool need_top_cards = 4;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 5;\n}\n\n//\nmessage RcmdPlaylistResp {\n  //\n  repeated DetailItem list = 1;\n  //\n  int64 history_len = 2;\n  //\n  repeated TopCard top_cards = 3;\n}\n\n//\nmessage ResponseUrl {\n  //\n  int32 order = 1;\n  //\n  int64 length = 2;\n  //\n  int64 size = 3;\n  //\n  string ahead = 4;\n  //\n  string vhead = 5;\n  //\n  string url = 6;\n  //\n  repeated string backup_url = 7;\n  //\n  string md5 = 8;\n}\n\n//\nmessage SortOption {\n  //\n  int32 order = 1;\n  //\n  int32 sort_field = 2;\n}\n\n//\nmessage ThumbUpReq {\n  //\n  PlayItem item = 1;\n  //\n  int32 action = 2;\n}\n\n//\nmessage ThumbUpResp {\n  //\n  string message = 1;\n}\n\n//\nmessage TopCard {\n  //\n  string title = 1;\n  //\n  int32 play_style = 2;\n  //\n  int32 card_type = 3;\n  //\n  oneof card {\n    //\n    TpcdHistory listen_history = 4;\n    //\n    TpcdFavFolder fav_folder = 5;\n    //\n    TpcdUpRecall up_recall = 6;\n    //\n    TpcdPickToday pick_today = 7;\n  }\n  //\n  int64 pos = 8;\n  //\n  string title_icon = 9;\n}\n\n//\nenum TopCardType {\n  UNSPECIFIED = 0;     //\n  LISTEN_HISTORY = 1;  //\n  FAVORITE_FOLDER = 2; //\n  UP_RECALL = 3;       //\n  PICK_TODAY = 4;      //\n}\n\n//\nmessage TpcdFavFolder {\n  //\n  DetailItem item = 1;\n  //\n  string text = 2;\n  //\n  string pic = 3;\n  //\n  int64 fid = 4;\n  //\n  int32 folder_type = 5;\n}\n\n//\nmessage TpcdHistory {\n  //\n  DetailItem item = 1;\n  //\n  string text = 2;\n  //\n  string pic = 3;\n}\n\n//\nmessage TpcdPickToday {\n  //\n  DetailItem item = 1;\n  //\n  string text = 2;\n  //\n  string pic = 3;\n  //\n  int64 pick_id = 4;\n  //\n  int64 pick_card_id = 5;\n}\n\n//\nmessage TpcdUpRecall {\n  //\n  int64 up_mid = 1;\n  //\n  string text = 2;\n  //\n  string avatar = 3;\n  //\n  int64 medialist_type = 4;\n  //\n  int64 medialist_biz_id = 5;\n  //\n  DetailItem item = 6;\n}\n\n//\nmessage TripleLikeReq {\n  //\n  PlayItem item = 1;\n}\n\n//\nmessage TripleLikeResp {\n  //\n  string message = 1;\n  //\n  bool thumb_ok = 2;\n  //\n  bool coin_ok = 3;\n  //\n  bool fav_ok = 4;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/playeronline/v1/playeronline.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.playeronline.v1;\n\noption java_multiple_files = true;\n\n// 在线人数\nservice PlayerOnline {\n  // 获取在线人数\n  rpc PlayerOnline (PlayerOnlineReq) returns (PlayerOnlineReply);\n  //\n  rpc PremiereInfo(PremiereInfoReq) returns (PremiereInfoReply);\n  //\n  rpc ReportWatch(ReportWatchReq) returns (NoReply);\n}\n\n// 空回复\nmessage NoReply {}\n\n// 获取在线人数-回复\nmessage PlayerOnlineReply {\n  //\n  string total_text = 1;\n  // 下次轮询间隔时间\n  int64 sec_next = 2;\n  // 是否底部显示\n  bool bottom_show = 3;\n  //\n  bool sdm_show = 4;\n  //\n  string sdm_text = 5;\n  //\n  int64 total_number = 6;\n  //\n  string total_number_text = 7;\n}\n\n// 获取在线人数-请求\nmessage PlayerOnlineReq {\n  // 稿件 avid\n  int64 aid = 1;\n  // 视频 cid\n  int64 cid = 2;\n  // 是否在播放中\n  bool play_open = 3;\n}\n\n//\nmessage PremiereInfoReply {\n  //\n  string premiere_over_text = 1;\n  //\n  int64 participant = 2;\n  //\n  int64 interaction = 3;\n}\n\n//\nmessage PremiereInfoReq {\n  //\n  int64 aid = 1;\n}\n\n//\nmessage ReportWatchReq {\n  //\n  int64 aid = 1;\n  //\n  string biz = 2;\n  //\n  string buvid = 3;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/playerunite/pgcanymodel/PGCAnyModel.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.playerunite.pgcanymodel;\n\noption java_multiple_files = true;\n\nimport \"bilibili/pgc/gateway/player/v2/playurl.proto\";\n\nmessage PGCAnyModel {\n  bilibili.pgc.gateway.player.v2.PlayViewBusinessInfo business = 3;\n  bilibili.pgc.gateway.player.v2.Event event = 4;\n  bilibili.pgc.gateway.player.v2.ViewInfo view_info = 5;\n  bilibili.pgc.gateway.player.v2.PlayAbilityExtConf play_ext_conf = 6;\n  //bilibili.pgc.gateway.player.v2.PlayExtInfo play_ext_info = 7;\n}\n\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/playerunite/ugcanymodel/UGCAnyModel.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.playerunite.ugcanymodel;\n\noption java_multiple_files = true;\n\nmessage ButtonStyle {\n  string text = 1;\n  string text_color = 2;\n  string bg_color = 3;\n  string jump_link = 4;\n}\n\nenum PlayLimitCode {\n  PLC_UNKNOWN = 0;\n  PLC_NOTPAYED = 1;\n}\n\nmessage PlayLimit {\n  PlayLimitCode code = 1;\n  string message = 2;\n  string sub_message = 3;\n  ButtonStyle button = 4;\n}\n\nmessage UGCAnyModel {\n  PlayLimit play_limit = 1;\n}\n\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/playerunite/v1/playerunite.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.playerunite.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/playershared/playershared.proto\";\n\nimport \"google/protobuf/any.proto\";\n\n// 统一视频url\nservice Player {\n  // 视频地址\n  rpc PlayViewUnite (PlayViewUniteReq) returns (PlayViewUniteReply);\n}\n\n// \nmessage PlayViewUniteReq {\n  // 请求资源VOD信息\n  bilibili.playershared.VideoVod vod = 1;\n  //\n  string spmid = 2;\n  //\n  string from_spmid = 3;\n  // 补充信息, 如ep_id等\n  map<string, string> extra_content = 4;\n  //\n  string bvid = 5;\n}\n\n// \nmessage PlayViewUniteReply {\n  // 音视频流信息\n  bilibili.playershared.VodInfo vod_info = 1;\n  //\n  bilibili.playershared.PlayArcConf play_arc_conf = 2;\n  //\n  bilibili.playershared.PlayDeviceConf play_device_conf = 3;\n  //\n  bilibili.playershared.Event event = 4;\n  // 使用 pgcanymodel / ugcanymodel 进行proto any转换成对应业务码结构体\n  google.protobuf.Any supplement = 5;\n  //\n  bilibili.playershared.PlayArc play_arc = 6;\n  //\n  bilibili.playershared.QnTrialInfo qn_trial_info = 7;\n  //\n  bilibili.playershared.History history = 8;\n  //\n  bilibili.playershared.ViewInfo view_info = 9;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/playurl/v1/playurl.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.playurl.v1;\n\noption java_multiple_files = true;\n\n// 视频url\nservice PlayURL {\n  // 视频地址\n  rpc PlayURL (PlayURLReq) returns (PlayURLReply);\n  // 投屏地址\n  rpc Project (ProjectReq) returns (ProjectReply);\n  // 播放页信息\n  rpc PlayView (PlayViewReq) returns (PlayViewReply);\n  // 编辑播放界面配置\n  rpc PlayConfEdit (PlayConfEditReq) returns (PlayConfEditReply);\n  // 获取播放界面配置\n  rpc PlayConf (PlayConfReq) returns (PlayConfReply);\n}\n\n//\nmessage AB {\n  //\n  Glance glance = 1;\n  //\n  int32 group = 2;\n}\n\n// 配置项\nmessage ArcConf {\n  // 是否支持\n  bool is_support = 1;\n  //\n  bool disabled = 2;\n  //\n  ExtraContent extra_content = 3;\n}\n\n// 业务类型\nenum Business {\n  UNKNOWN = 0; // 未知类型\n  STORY = 1; // story业务\n}\n\n// Chronos灰度管理\nmessage Chronos {\n  // 资源md5\n  string md5 = 1;\n  // 资源文件\n  string file = 2;\n}\n\n//\nmessage ButtonStyle {\n  //\n  string text = 1;\n  //\n  string text_color = 2;\n  //\n  string bg_color = 3;\n  //\n  string jump_link = 4;\n}\n\n//\nmessage CloudConf {\n  // 是否展示功能\n  bool show = 1;\n  // 设置类型\n  ConfType conf_type = 2;\n  //\n  FieldValue field_value = 3;\n  //\n  ConfValue conf_value = 4;\n}\n\n// 编码类型\nenum CodeType {\n  NOCODE = 0; // 默认\n  CODE264 = 1; // H.264\n  CODE265 = 2; // H.265\n  CODEAV1 = 3; // av1\n}\n\n// 设置类型\nenum ConfType {\n  NoType = 0; //\n  FLIPCONF = 1; // 镜像反转\n  CASTCONF = 2; // 视频投屏\n  FEEDBACK = 3; // 反馈\n  SUBTITLE = 4; // 字幕\n  PLAYBACKRATE = 5; // 播放速度\n  TIMEUP = 6; // 定时停止播放\n  PLAYBACKMODE = 7; // 播放方式\n  SCALEMODE = 8; // 画面尺寸\n  BACKGROUNDPLAY = 9; // 后台播放\n  LIKE = 10; // 顶\n  DISLIKE = 11; // 踩\n  COIN = 12; // 投币\n  ELEC = 13; // 充电\n  SHARE = 14; // 分享\n  SCREENSHOT = 15; // 截图\n  LOCKSCREEN = 16; // 锁屏\n  RECOMMEND = 17; // 推荐\n  PLAYBACKSPEED = 18; // 倍速\n  DEFINITION = 19; // 清晰度\n  SELECTIONS = 20; // 选集\n  NEXT = 21; // 下一集\n  EDITDM = 22; // 编辑弹幕\n  SMALLWINDOW = 23; // 小窗\n  SHAKE = 24; // 播放震动\n  OUTERDM = 25; // 外层面板弹幕设置\n  INNERDM = 26; // 三点内弹幕设置\n  PANORAMA = 27; // 全景\n  DOLBY = 28; // 杜比\n  COLORFILTER = 29; // 颜色滤镜\n}\n\n//\nmessage ConfValue {\n  oneof value {\n    //\n    bool switch_val = 1;\n    //\n    int64 selected_val = 2;\n  }\n}\n\n// dash条目\nmessage DashItem {\n  // 清晰度\n  uint32 id = 1;\n  // 主线流\n  string baseUrl = 2;\n  // 备用流\n  repeated string backup_url = 3;\n  // 带宽\n  uint32 bandwidth = 4;\n  // 编码id\n  uint32 codecid = 5;\n  // md5\n  string md5 = 6;\n  // 大小\n  uint64 size = 7;\n  // 帧率\n  string frame_rate = 8;\n  //\n  string widevine_pssh = 9;\n}\n\n// dash视频流\nmessage DashVideo {\n  // 主线流\n  string base_url = 1;\n  // 备用流\n  repeated string backup_url = 2;\n  // 带宽\n  uint32 bandwidth = 3;\n  // 编码id\n  uint32 codecid = 4;\n  // md5\n  string md5 = 5;\n  // 大小\n  uint64 size = 6;\n  // 伴音质量id\n  uint32 audioId = 7;\n  // 是否非全二压\n  bool no_rexcode = 8;\n  // 码率\n  string frame_rate = 9;\n  // 宽度\n  int32 width = 10;\n  // 高度\n  int32 height = 11;\n  //\n  string widevine_pssh = 12;\n}\n\n// 杜比伴音信息\nmessage DolbyItem {\n  enum Type {\n    NONE = 0; // NONE\n    COMMON = 1; // 普通杜比音效\n    ATMOS = 2; // 全景杜比音效\n  }\n  // 杜比类型\n  Type type = 1;\n  // 音频流\n  DashItem audio = 2;\n}\n\n// 事件\nmessage Event {\n  // 震动\n  Shake shake = 1;\n}\n\n//\nmessage ExtraContent {\n  //\n  string disabled_reason = 1;\n  //\n  int64 disabled_code = 2;\n}\n\n// 配置字段值\nmessage FieldValue {\n  oneof value {\n    // 开关\n    bool switch = 1;\n  }\n}\n\n// 清晰度描述\nmessage FormatDescription {\n  // 清晰度\n  int32 quality = 1;\n  // 清晰度格式\n  string format = 2;\n  // 清晰度描述\n  string description = 3;\n  // 新描述\n  string new_description = 4;\n  // 选中态的清晰度描述\n  string display_desc = 5;\n  // 选中态的清晰度描述的角标\n  string superscript = 6;\n}\n\n//\nmessage Glance {\n  //\n  bool can_watch = 1;\n  //\n  int64 times = 2;\n  //\n  int64 duration = 3;\n}\n\n//\nenum Group {\n  UnknownGroup = 0; //\n  A = 1; //\n  B = 2; //\n  C = 3; //\n}\n\n// 禁用功能配置\nmessage PlayAbilityConf {\n  CloudConf background_play_conf = 1;  // 后台播放\n  CloudConf flip_conf = 2;  // 镜像反转\n  CloudConf cast_conf = 3;  // 投屏\n  CloudConf feedback_conf = 4;  // 反馈\n  CloudConf subtitle_conf = 5;  // 字幕\n  CloudConf playback_rate_conf = 6;  // 播放速度\n  CloudConf time_up_conf = 7;  // 定时停止\n  CloudConf playback_mode_conf = 8;  // 播放方式\n  CloudConf scale_mode_conf = 9;  // 画面尺寸\n  CloudConf like_conf = 10; // 赞\n  CloudConf dislike_conf = 11; // 踩\n  CloudConf coin_conf = 12; // 投币\n  CloudConf elec_conf = 13; // 充电\n  CloudConf share_conf = 14; // 分享\n  CloudConf screen_shot_conf = 15; // 截图\n  CloudConf lock_screen_conf = 16; // 锁定\n  CloudConf recommend_conf = 17; // 相关推荐\n  CloudConf playback_speed_conf = 18; // 播放速度\n  CloudConf definition_conf = 19; // 清晰度\n  CloudConf selections_conf = 20; // 选集\n  CloudConf next_conf = 21; // 下一集\n  CloudConf edit_dm_conf = 22; // 编辑弹幕\n  CloudConf small_window_conf = 23; // 小窗\n  CloudConf shake_conf = 24; // 震动\n  CloudConf outer_dm_conf = 25; // 外层面板弹幕设置\n  CloudConf innerDmDisable = 26; // 三点内弹幕设置\n  CloudConf inner_dm_conf = 27; // 一起看入口\n  CloudConf dolby_conf = 28; // 杜比音效\n  CloudConf color_filter_conf = 29; // 颜色滤镜\n}\n\n// 播放控件稿件配置\nmessage PlayArcConf {\n  ArcConf background_play_conf = 1;  // 后台播放\n  ArcConf flip_conf = 2;  // 镜像反转\n  ArcConf cast_conf = 3;  // 投屏\n  ArcConf feedback_conf = 4;  // 反馈\n  ArcConf subtitle_conf = 5;  // 字幕\n  ArcConf playback_rate_conf = 6;  // 播放速度\n  ArcConf time_up_conf = 7;  // 定时停止\n  ArcConf playback_mode_conf = 8;  // 播放方式\n  ArcConf scale_mode_conf = 9;  // 画面尺寸\n  ArcConf like_conf = 10; // 赞\n  ArcConf dislike_conf = 11; // 踩\n  ArcConf coin_conf = 12; // 投币\n  ArcConf elec_conf = 13; // 充电\n  ArcConf share_conf = 14; // 分享\n  ArcConf screen_shot_conf = 15; // 截图\n  ArcConf lock_screen_conf = 16; // 锁定\n  ArcConf recommend_conf = 17; // 相关推荐\n  ArcConf playback_speed_conf = 18; // 播放速度\n  ArcConf definition_conf = 19; // 清晰度\n  ArcConf selections_conf = 20; // 选集\n  ArcConf next_conf = 21; // 下一集\n  ArcConf edit_dm_conf = 22; // 编辑弹幕\n  ArcConf small_window_conf = 23; // 小窗\n  ArcConf shake_conf = 24; // 震动\n  ArcConf outer_dm_conf = 25; // 外层面板弹幕设置\n  ArcConf inner_dm_conf = 26; // 三点内弹幕设置\n  ArcConf panorama_conf = 27; // 一起看入口\n  ArcConf dolby_conf = 28; // 杜比音效\n  ArcConf screen_recording_conf = 29; // 屏幕录制\n  ArcConf color_filter_conf = 30; // 颜色滤镜\n}\n\n// 编辑播放界面配置-响应\nmessage PlayConfEditReply {\n\n}\n\n// 编辑播放界面配置-请求\nmessage PlayConfEditReq {\n  // 播放界面配置\n  repeated PlayConfState play_conf = 1;\n}\n\n// 获取播放界面配置-响应\nmessage PlayConfReply {\n  //播放控件用户自定义配置\n  PlayAbilityConf play_conf = 1;\n}\n\n// 获取播放界面配置-请求\nmessage PlayConfReq {\n\n}\n\n// 播放界面配置\nmessage PlayConfState {\n  // 设置类型\n  ConfType conf_type = 1;\n  // 是否隐藏\n  bool show = 2;\n  // 配置字段值\n  FieldValue field_value = 3;\n  //\n  ConfValue conf_value = 4;\n}\n\n// 错误码\nenum PlayErr {\n  NoErr = 0; //\n  WithMultiDeviceLoginErr = 1; // 管控类型的错误码\n}\n\n//\nmessage PlayLimit {\n  //\n  PlayLimitCode code = 1;\n  //\n  string message = 2;\n  //\n  string sub_message = 3;\n  //\n  ButtonStyle button = 4;\n}\n\n//\nenum PlayLimitCode {\n  PLCUnkown = 0; //\n  PLCUgcNotPayed = 1; //\n}\n\n// 视频地址-回复\nmessage PlayURLReply {\n  // 清晰的\n  uint32 quality = 1;\n  // 格式\n  string format = 2;\n  // 总时长(单位为ms)\n  uint64 timelength = 3;\n  // 编码id\n  uint32 video_codecid = 4;\n  // 视频流版本\n  uint32 fnver = 5;\n  // 视频流格式\n  uint32 fnval = 6;\n  // 是否支持投影\n  bool video_project = 7;\n  // 分段视频流列表\n  repeated ResponseUrl durl = 8;\n  // dash数据\n  ResponseDash dash = 9;\n  // 是否非全二压\n  int32 no_rexcode = 10;\n  // 互动视频升级提示\n  UpgradeLimit upgrade_limit = 11;\n  // 清晰度描述列表\n  repeated FormatDescription support_formats = 12;\n  // 视频格式\n  VideoType type = 13;\n}\n\n// 视频地址-请求\nmessage PlayURLReq {\n  // 稿件avid\n  int64 aid = 1;\n  // 视频cid\n  int64 cid = 2;\n  // 清晰度\n  int64 qn = 3;\n  // 视频流版本\n  int32 fnver = 4;\n  // 视频流格式\n  int32 fnval = 5;\n  // 下载模式\n  // 0:播放 1:flv下载 2:dash下载\n  uint32 download = 6;\n  // 流url强制是用域名\n  // 0:允许使用ip 1:使用http 2:使用https\n  int32 force_host = 7;\n  // 是否4K\n  bool fourk = 8;\n  // 当前页spm\n  string spmid = 9;\n  // 上一页spm\n  string from_spmid = 10;\n}\n\n// 播放页信息-回复\nmessage PlayViewReply {\n  // 视频流信息\n  VideoInfo video_info = 1;\n  // 播放控件用户自定义配置\n  PlayAbilityConf play_conf = 2;\n  // 互动视频升级提示\n  UpgradeLimit upgrade_limit = 3;\n  // Chronos灰度管理\n  Chronos chronos = 4;\n  // 播放控件稿件配置\n  PlayArcConf play_arc = 5;\n  // 事件\n  Event event = 6;\n  //\n  AB ab = 7;\n  //\n  PlayLimit play_limit = 8;\n}\n\n// 播放页信息-请求\nmessage PlayViewReq {\n  // 稿件avid\n  int64 aid = 1;\n  // 视频cid\n  int64 cid = 2;\n  // 清晰度\n  int64 qn = 3;\n  // 视频流版本\n  int32 fnver = 4;\n  // 视频流格式\n  int32 fnval = 5;\n  // 下载模式\n  // 0:播放 1:flv下载 2:dash下载\n  uint32 download = 6;\n  // 流url强制是用域名\n  // 0:允许使用ip 1:使用http 2:使用https\n  int32 force_host = 7;\n  // 是否4K\n  bool fourk = 8;\n  // 当前页spm\n  string spmid = 9;\n  // 上一页spm\n  string from_spmid = 10;\n  // 青少年模式\n  int32 teenagers_mode = 11;\n  // 编码\n  CodeType prefer_codec_type = 12;\n  // 业务类型\n  Business business = 13;\n  //\n  int64 voice_balance = 14;\n}\n\n// 投屏地址-响应\nmessage ProjectReply {\n  PlayURLReply project = 1;\n}\n\n// 投屏地址-请求\nmessage ProjectReq {\n  // 稿件avid\n  int64 aid = 1;\n  // 视频cid\n  int64 cid = 2;\n  // 清晰度\n  int64 qn = 3;\n  // 视频流版本\n  int32 fnver = 4;\n  // 视频流格式\n  int32 fnval = 5;\n  // 下载模式\n  // 0:播放 1:flv下载 2:dash下载\n  uint32 download = 6;\n  // 流url强制是用域名\n  // 0:允许使用ip 1:使用http 2:使用https\n  int32 force_host = 7;\n  // 是否4K\n  bool fourk = 8;\n  // 当前页spm\n  string spmid = 9;\n  // 上一页spm\n  string from_spmid = 10;\n  // 使用协议\n  // 0:默认乐播 1:自建协议 2:云投屏 3:airplay\n  int32 protocol = 11;\n  // 投屏设备\n  // 0:默认其他 1:OTT设备\n  int32 device_type = 12;\n}\n\n// dash数据\nmessage ResponseDash {\n  // dash视频流\n  repeated DashItem video = 1;\n  // dash伴音流\n  repeated DashItem audio = 2;\n}\n\n// 分段流条目\nmessage ResponseUrl {\n  // 分段序号\n  uint32 order = 1;\n  // 分段时长\n  uint64 length = 2;\n  // 分段大小\n  uint64 size = 3;\n  // 主线流\n  string url = 4;\n  // 备用流\n  repeated string backup_url = 5;\n  // md5\n  string md5 = 6;\n}\n\n//分段视频流\nmessage SegmentVideo {\n  //分段视频流列表\n  repeated ResponseUrl segment = 1;\n}\n\n// 震动\nmessage Shake {\n  // 文件地址\n  string file = 1;\n}\n\n// 视频流信息\nmessage Stream {\n  // 元数据\n  StreamInfo stream_info = 1;\n  // 流数据\n  oneof content {\n    // dash流\n    DashVideo dash_video = 2;\n    // 分段流\n    SegmentVideo segment_video = 3;\n  }\n}\n\n// 流媒体元数据\nmessage StreamInfo {\n  // 清晰度\n  uint32 quality = 1;\n  // 格式\n  string format = 2;\n  // 格式描述\n  string description = 3;\n  // 错误码\n  PlayErr err_code = 4;\n  // 不满足条件信息\n  StreamLimit limit = 5;\n  // 是否需要vip\n  bool need_vip = 6;\n  // 是否需要登录\n  bool need_login = 7;\n  // 是否完整\n  bool intact = 8;\n  // 是否非全二压\n  bool no_rexcode = 9;\n  // 清晰度属性位\n  int64 attribute = 10;\n  // 新版格式描述\n  string new_description = 11;\n  // 格式文字\n  string display_desc = 12;\n  // 新版格式描述备注\n  string superscript = 13;\n}\n\n// 清晰度不满足条件信息\nmessage StreamLimit {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 提示信息\n  string msg = 3;\n}\n\n// 互动视频升级按钮信息\nmessage UpgradeButton {\n  // 标题\n  string title = 1;\n  // 链接\n  string link = 2;\n}\n\n// 互动视频升级提示\nmessage UpgradeLimit {\n  // 错误码\n  int32 code = 1;\n  // 错误信息\n  string message = 2;\n  // 图片url\n  string image = 3;\n  // 按钮信息\n  UpgradeButton button = 4;\n}\n\n// 视频url信息\nmessage VideoInfo {\n  // 视频清晰度\n  uint32 quality = 1;\n  // 视频格式\n  string format = 2;\n  // 视频时长\n  uint64 timelength = 3;\n  // 视频编码id\n  uint32 video_codecid = 4;\n  // 视频流\n  repeated Stream stream_list = 5;\n  // 伴音流\n  repeated DashItem dash_audio = 6;\n  // 杜比伴音流\n  DolbyItem dolby = 7;\n  //\n  VolumeInfo volume = 8;\n}\n\n// 视频类型\nenum VideoType {\n  Unknown_VALUE = 0; //\n  FLV_VALUE = 1; // flv格式\n  DASH_VALUE = 2; // dash格式\n  MP4_VALUE = 3; // mp4格式\n}\n\n//\nmessage VolumeInfo {\n  //\n  double measured_i = 1;\n  //\n  double measured_lra = 2;\n  //\n  double measured_tp = 3;\n  //\n  double measured_threshold = 4;\n  //\n  double target_offset = 5;\n  //\n  double target_i = 6;\n  //\n  double target_tp = 7;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/resource/privacy/v1/api.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.resource.privacy.v1;\n\noption java_multiple_files = true;\n\n// 隐私\nservice Privacy {\n  // 获取隐私设置\n  rpc PrivacyConfig(NoArgRequest) returns(PrivacyConfigReply);\n  // 修改隐私设置\n  rpc SetPrivacyConfig(SetPrivacyConfigRequest) returns(NoReply);\n}\n\n// 空请求\nmessage NoArgRequest{\n\n}\n\n// 空响应\nmessage NoReply{\n\n}\n\n// 隐私设置\nmessage PrivacyConfigItem {\n  // 隐私开关类型\n  PrivacyConfigType privacy_config_type = 1;\n  //\n  string title = 2;\n  // 隐私开关状态\n  PrivacyConfigState state = 3;\n  //\n  string sub_title = 4;\n  //\n  string sub_title_uri = 5;\n}\n\n// 获取隐私设置-响应\nmessage PrivacyConfigReply {\n  // 隐私设置\n  PrivacyConfigItem privacy_config_item = 1;\n}\n\n// 隐私开关状态\nenum PrivacyConfigState {\n  close = 0; // 关闭\n  open = 1; // 打开\n}\n\n// 隐私开关类型\nenum PrivacyConfigType {\n  none = 0; //\n  dynamic_city = 1; // 动态同城\n}\n\n// 修改隐私设置-请求\nmessage SetPrivacyConfigRequest {\n  // 隐私开关类型\n  PrivacyConfigType privacy_config_type = 1;\n  // 隐私开关状态\n  PrivacyConfigState state = 2;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/resource/v1/module.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.resource.v1;\n\noption java_multiple_files = true;\n\n//\nservice Module {\n  //\n  rpc List(ListReq) returns (ListReply);\n}\n\n//\nenum CompressType {\n  Unzip = 0; // unzip\n  Original = 1; // 不操作\n}\n\n//\nenum EnvType {\n  Unknown = 0; //\n  Release = 1; //\n  Test = 2; //\n}\n\n//\nenum IncrementType {\n  Total = 0; // 全量包\n  Incremental = 1; // 增量包\n}\n\n//\nenum LevelType {\n  Undefined = 0; //\n  High = 1; // 高 需立即下载\n  Middle = 2; // 中 可以延迟下载\n  Low = 3; // 低 仅在业务方使用到时由业务方手动进行下载\n}\n\nmessage ListReply {\n  //\n  string env = 1;\n  //\n  repeated PoolReply pools = 2;\n  //\n  int64 list_version = 3;\n}\n\n//\nmessage ListReq {\n  //\n  string pool_name = 1;\n  //\n  string module_name = 2;\n  //\n  repeated VersionListReq version_list = 3;\n  //\n  EnvType env = 4;\n  //\n  int32 sys_ver = 5;\n  //\n  int32 scale = 6;\n  //\n  int32 arch = 7;\n  //\n  int64 list_version = 8;\n}\n\n//\nmessage ModuleReply {\n  //\n  string name = 1;\n  //\n  int64 version = 2;\n  //\n  string url = 3;\n  //\n  string md5 = 4;\n  //\n  string total_md5 = 5;\n  //\n  IncrementType increment = 6;\n  //\n  bool is_wifi = 7;\n  //\n  LevelType level = 8;\n  //\n  string filename = 9;\n  //\n  string file_type = 10;\n  //\n  int64 file_size = 11;\n  //\n  CompressType compress = 12;\n  //\n  int64 publish_time = 13;\n  // 上报使用\n  int64 pool_id = 14;\n  //\n  int64 module_id = 15;\n  //\n  int64 version_id = 16;\n  //\n  int64 file_id = 17;\n  //\n  bool zip_check = 18;\n}\n\nmessage PoolReply {\n  //\n  string name = 1;\n  //\n  repeated ModuleReply modules = 2;\n}\n\n//\nmessage VersionListReq {\n  //\n  string pool_name = 1;\n  //\n  repeated VersionReq versions = 2;\n}\n\n//\nmessage VersionReq {\n  //\n  string module_name = 1;\n  //\n  int64 version = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/search/v2/search.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.search.v2;\n\noption java_multiple_files = true;\n\nimport \"bilibili/broadcast/message/main/search.proto\";\n\nservice Search {\n  //\n  rpc CancelChatTask (CancelChatTaskReq) returns (CancelChatTaskReply);\n  //\n  rpc GetChatResult (GetChatResultReq) returns (bilibili.broadcast.message.main.ChatResult);\n  //\n  rpc SearchEgg (SearchEggReq) returns (SearchEggReply);\n  //\n  rpc SubmitChatTask (SubmitChatTaskReq) returns (SubmitChatTaskReply);\n}\n\n// \nmessage CancelChatTaskReq {\n  //\n  string session_id = 1;\n  //\n  string from_source = 2;\n}\n\n// \nmessage CancelChatTaskReply {\n  //\n  int32 code = 1;\n}\n\n// \nmessage GetChatResultReq {\n  //\n  string query = 1;\n  //\n  string session_id = 2;\n  //\n  string from_source = 3;\n}\n\n// \nmessage SearchEggInfo {\n  //\n  int32 egg_type = 1;\n  //\n  int64 id = 2;\n  //\n  int32 is_commercial = 3;\n  //\n  string mask_color = 4;\n  //\n  int64 mask_transparency = 5;\n  //\n  string md5 = 6;\n  //\n  int32 re_type = 7;\n  //\n  string re_url = 8;\n  //\n  string re_value = 9;\n  //\n  int32 show_count = 10;\n  //\n  int64 size = 11;\n  //\n  int64 source = 12;\n  //\n  string url = 13;\n}\n\n// \nmessage SearchEggInfos {\n  //\n  repeated SearchEggInfo egg_info = 1;\n}\n\n// \nmessage SearchEggReply {\n  //\n  int32 code = 1;\n  //\n  string seid = 2;\n  //\n  SearchEggInfos result = 3;\n}\n\n// \nmessage SearchEggReq {\n\n}\n\n// \nmessage SubmitChatTaskReply {\n  //\n  int32 code = 1;\n  //\n  string session_id = 2;\n}\n\n// \nmessage SubmitChatTaskReq {\n  //\n  string query = 1;\n  //\n  string track_id = 2;\n  //\n  string from_source = 3;\n}\n\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/show/gateway/v1/service.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.show.gateway.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/broadcast/message/main/native.proto\";\n\n//\nservice AppShow {\n  // 获取Native页进度数据\n  rpc GetActProgress (GetActProgressReq) returns (GetActProgressReply);\n}\n\n// 获取Native页进度数据-请求\nmessage GetActProgressReq {\n  // Native页id\n  int64 pageID = 1;\n  // 用户mid\n  int64 mid = 2;\n}\n\n// 获取Native页进度数据-响应\nmessage GetActProgressReply {\n  // 进度数据\n  bilibili.broadcast.message.main.NativePageEvent event = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/show/mixture/v1/mixture.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.show.mixture.v1;\n\noption java_multiple_files = true;\n\n// \nservice Mixture {\n  //\n  rpc Widget(WidgetReq) returns (WidgetReply);\n}\n\n// \nmessage RcmdReason {\n  //\n  string content = 1;\n  //\n  uint32 corner_mark = 2;\n}\n\n// \nmessage WidgetItem {\n  //\n  string cover = 1;\n  //\n  string view = 2;\n  //\n  RcmdReason rcmd_reason = 3;\n  //\n  string title = 4;\n  //\n  string name = 5;\n  //\n  string uri = 6;\n  //\n  string goto = 7;\n  //\n  int64 id = 8;\n  //\n  int32 view_icon = 9;\n}\n\n// \nmessage WidgetReply {\n  //\n  repeated WidgetItem item = 1;\n}\n\n// \nmessage WidgetReq {\n  //\n  string from_spmid = 1;\n  //\n  uint32 page_no = 2;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/show/popular/v1/popular.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.show.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/card/v1/card.proto\";\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\n\n// 热门\nservice Popular {\n  // 热门列表\n  rpc Index (PopularResultReq) returns (PopularReply);\n}\n\n// 气泡信息\nmessage Bubble {\n  // 文案\n  string bubble_content = 1;\n  // 版本\n  int32 version = 2;\n  // 起始时间\n  int64 stime = 3;\n}\n\n// 配置信息\nmessage Config {\n  // 标题\n  string item_title = 1;\n  // 底部文案\n  string bottom_text = 2;\n  // 底部图片url\n  string bottom_text_cover = 3;\n  // 底部跳转页url\n  string bottom_text_url = 4;\n  // 顶部按钮信息列表\n  repeated EntranceShow top_items = 5;\n  // 头图url\n  string head_image = 6;\n  // 当前页按钮信息\n  repeated EntranceShow page_items = 7;\n  //\n  int32 hit = 8;\n}\n\n// 按钮信息\nmessage EntranceShow {\n  // 按钮图标url\n  string icon = 1;\n  // 按钮名\n  string title = 2;\n  // 入口模块id\n  string module_id = 3;\n  // 跳转uri\n  string uri = 4;\n  // 气泡信息\n  Bubble bubble = 5;\n  // 入口id\n  int64 entrance_id = 6;\n  // 头图url\n  string top_photo = 7;\n  // 入口类型\n  int32 entrance_type = 8;\n}\n\n// 热门列表-响应\nmessage PopularReply {\n  // 卡片列表\n  repeated bilibili.app.card.v1.Card items = 1;\n  // 配置信息\n  Config config = 2;\n  // 版本\n  string ver = 3;\n}\n\n// 热门列表-请求\nmessage PopularResultReq {\n  // 排位索引id，为上此请求末尾项的idx\n  int64 idx = 1;\n  // 登录标识\n  // 1:未登陆用户第一页 2:登陆用户第一页\n  int32 login_event = 2;\n  // 清晰度(旧版)\n  int32 qn = 3;\n  // 视频流版本(旧版)\n  int32 fnver = 4;\n  // 视频流功能(旧版)\n  int32 fnval = 5;\n  // 是否强制使用域名(旧版)\n  int32 force_host = 6;\n  // 是否4K(旧版)\n  int32 fourk = 7;\n  // 当前页面spm\n  string spmid = 8;\n  // 上此请求末尾项的param\n  string last_param = 9;\n  // 上此请求的ver\n  string ver = 10;\n  // 分品类热门的入口ID\n  int64 entrance_id = 11;\n  // 热门定位id集合\n  string location_ids = 12;\n  // 0:tag页 1:中间页\n  int32 source_id = 13;\n  // 数据埋点上报\n  // 0:代表手动刷新 1:代表自动刷新\n  int32 flush = 14;\n  // 秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 15;\n}\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/show/rank/v1/rank.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.show.v1;\n\noption java_multiple_files = true;\n\n// 排行榜\nservice Rank {\n  // 全站排行榜\n  rpc RankAll (RankAllResultReq) returns (RankListReply);\n  // 分区排行榜\n  rpc RankRegion (RankRegionResultReq) returns (RankListReply);\n}\n\n// 排行榜列表项\nmessage Item {\n  // 标题\n  string title = 1;\n  // 封面url\n  string cover = 2;\n  // 参数(稿件avid)\n  string param = 3;\n  // 跳转uri\n  string uri = 4;\n  // 重定向url\n  string redirect_url = 5;\n  // 跳转类型\n  // av:视频稿件\n  string goto = 6;\n  // 播放数\n  int64 play = 7;\n  // 弹幕数\n  int32 danmaku = 8;\n  // UP主mid\n  int64 mid = 9;\n  // UP主昵称\n  string name = 10;\n  // UP主头像url\n  string face = 11;\n  // 评论数\n  int32 reply = 12;\n  // 收藏数\n  int32 favourite = 13;\n  // 发布时间\n  int64 pub_date = 14;\n  // 分区tid\n  int32 rid = 15;\n  // 子分区名\n  string rname = 16;\n  // 视频总时长\n  int64 duration = 17;\n  // 点赞数\n  int32 like = 18;\n  // 1P cid\n  int64 cid = 19;\n  // 综合评分\n  int64 pts = 20;\n  // 合作视频文案\n  string cooperation = 21;\n  // 属性位\n  // 0:未关注 1:已关注\n  int32 attribute = 22;\n  // UP主粉丝数\n  int64 follower = 23;\n  // UP主认证信息\n  OfficialVerify official_verify = 24;\n  // 同一UP收起子项列表\n  repeated Item children = 25;\n  // 关系信息\n  Relation relation = 26;\n}\n\n// 认证信息\nmessage OfficialVerify {\n  // 认证类型\n  // -1:无认证 0:个人认证 1:机构认证\n  int32 type = 1;\n  // 认证描述\n  string desc = 2;\n}\n\n// 全站排行榜-请求\nmessage RankAllResultReq {\n  // 必须为\"all\"\n  string order = 1;\n  // 页码\n  // 默认1页\n  int32 pn = 2;\n  // 每页项数\n  // 默认100项，最大100\n  int32 ps = 3;\n}\n\n// 排行榜信息-响应\nmessage RankListReply {\n  // 排行榜列表\n  repeated Item items = 1;\n}\n\n// 分区排行榜-请求\nmessage RankRegionResultReq {\n  // 一级分区tid(二级分区不可用)\n  // 0:全站\n  int32 rid = 1;\n  // 页码\n  // 默认1页\n  int32 pn = 2;\n  // 每页项数\n  // 默认100项，最大100\n  int32 ps = 3;\n}\n\n// 关系信息\nmessage Relation {\n  // 关系状态id\n  // 1:未关注 2:已关注 3:被关注 4:互相关注\n  int32 status = 1;\n  // 是否关注\n  int32 is_follow = 2;\n  // 是否粉丝\n  int32 is_followed = 3;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/show/region/v1/region.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.show.region.v1;\n\noption java_multiple_files = true;\n\n//\nservice Region {\n  //\n  rpc Region (RegionReq) returns (RegionReply);\n}\n\n//\nmessage RegionConfig {\n  //\n  string scenes_name = 1;\n  //\n  string scenes_type = 2;\n}\n\n//\nmessage RegionInfo {\n  //\n  int32 tid = 1;\n  //\n  int32 reid = 2;\n  //\n  string name = 3;\n  //\n  string logo = 4;\n  //\n  string goto = 5;\n  //\n  string param = 6;\n  //\n  string uri = 7;\n  //\n  int32 type = 8;\n  //\n  int32 is_bangumi = 9;\n  //\n  repeated RegionInfo children = 10;\n  //\n  repeated RegionConfig config = 11;\n}\n\n//\nmessage RegionReply {\n  //\n  repeated RegionInfo regions = 1;\n}\n\n//\nmessage RegionReq {\n  //\n  string lang = 1;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/space/v1/space.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.space.v1;\n\noption java_multiple_files = true;\n\n//\nservice Space {\n  //\n  rpc Archive (ArchiveReq) returns (ArchiveReply);\n}\n\n//-响应\nmessage ArchiveReply {\n  //\n  repeated BiliSpaceVideo item = 1;\n  //\n  int32 count = 2;\n  //\n  EpisodicButton episodic_button = 3;\n  //\n  repeated OrderConfig order = 4;\n}\n\n//-请求\nmessage ArchiveReq {\n  //\n  int64 vmid = 1;\n  //\n  int32 pn = 2;\n  //\n  int32 ps = 3;\n  //\n  string order = 4;\n}\n\n//\nmessage Badge {\n  //\n  string text = 1;\n  //\n  string text_color = 2;\n  //\n  string text_color_night = 3;\n  //\n  string bg_color = 4;\n  //\n  string bg_color_night = 5;\n  //\n  string border_color = 6;\n  //\n  string border_color_night = 7;\n  //\n  int32 bg_style = 8;\n}\n\n//\nmessage BiliSpaceVideo {\n  //\n  string title = 1;\n  //\n  string tname = 2;\n  //\n  int64 duration = 3;\n  //\n  string cover = 4;\n  //\n  string uri = 5;\n  //\n  string param = 6;\n  //\n  string danmaku = 7;\n  //\n  int64 play = 8;\n  //\n  int64 ctime = 9;\n  //\n  bool state = 10;\n  //\n  bool is_popular = 11;\n  //\n  repeated Badge badges = 12;\n  //\n  string cover_right = 13;\n  //\n  string bvid = 14;\n  //\n  bool is_steins = 15;\n  //\n  bool is_ugcpay = 16;\n  //\n  bool is_cooperation = 17;\n}\n\n//\nmessage EpisodicButton {\n  //\n  string text = 1;\n  //\n  string uri = 2;\n}\n\n//\nmessage OrderConfig {\n  //\n  string title = 1;\n  //\n  string value = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/splash/v1/splash.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.splash.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/any.proto\";\n\n//\nservice Splash {\n  //\n  rpc List (SplashReq) returns (SplashReply);\n}\n\n//\nmessage ShowStrategy {\n  //\n  int32 id = 1;\n  //\n  int64 stime = 2;\n  //\n  int64 etime = 3;\n}\n\n//\nmessage SplashItem {\n  //\n  int32 id = 1;\n  //\n  int32 type = 2;\n  //\n  int32 card_type = 3;\n  //\n  int32 duration = 4;\n  //\n  int64 begin_time = 5;\n  //\n  int64 end_time = 6;\n  //\n  string thumb = 7;\n  //\n  string hash = 8;\n  //\n  string logo_url = 9;\n  //\n  string logo_hash = 10;\n  //\n  string video_url = 11;\n  //\n  string video_hash = 12;\n  //\n  int32 video_width = 13;\n  //\n  int32 video_height = 14;\n  //\n  string schema = 15;\n  //\n  string schema_title = 16;\n  //\n  string schema_package_name = 17;\n  //\n  repeated string schema_callup_whiteList = 18;\n  //\n  int32 skip = 19;\n  //\n  string uri = 20;\n  //\n  string uri_title = 21;\n  //\n  int32 source = 22;\n  //\n  int32 cm_mark = 23;\n  //\n  string ad_cb = 24;\n  //\n  int64 resource_id = 25;\n  //\n  string request_id = 26;\n  //\n  string client_ip = 27;\n  //\n  bool is_ad = 28;\n  //\n  bool is_ad_loc = 29;\n  //\n  google.protobuf.Any extra = 30;\n  //\n  int64 card_index = 31;\n  //\n  int64 server_type = 32;\n  //\n  int64 index = 33;\n  //\n  string click_url = 34;\n  //\n  string show_url = 35;\n  //\n  int32 time_target = 36;\n  //\n  int32 encryption = 37;\n  //\n  bool enable_pre_download = 38;\n  //\n  bool enable_background_download = 39;\n}\n\n//-响应\nmessage SplashReply {\n  //\n  int32 max_time = 1;\n  //\n  int32 min_interval = 2;\n  //\n  int32 pull_interval = 3;\n  //\n  repeated SplashItem list = 4;\n  //\n  repeated ShowStrategy show = 5;\n}\n\n//-请求\nmessage SplashReq {\n  //\n  int32 width = 1;\n  //\n  int32 height = 2;\n  //\n  string birth = 3;\n  //\n  string ad_extra = 4;\n  //\n  string network = 5;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/topic/v1/topic.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.topic.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/dynamic/v2/dynamic.proto\";\nimport \"bilibili/app/card/v1/common.proto\";\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\n\n//\nservice Topic {\n  //\n  rpc TopicDetailsAll(TopicDetailsAllReq) returns (TopicDetailsAllReply);\n  //\n  rpc TopicDetailsFold(TopicDetailsFoldReq) returns (TopicDetailsFoldReply);\n  //\n  rpc TopicSetDetails(TopicSetDetailsReq) returns (TopicSetDetailsReply);\n}\n\n//\nmessage ButtonMeta {\n  //\n  string text = 1;\n  //\n  string icon = 2;\n}\n\n//\nmessage DetailsTopInfo {\n  //\n  TopicInfo topic_info = 1;\n  //\n  User user = 2;\n  //\n  string stats_desc = 3;\n  //\n  bool has_create_jurisdiction = 4;\n  //\n  OperationContent operation_content = 5;\n  //\n  string head_img_url = 6;\n  //\n  string head_img_backcolor = 7;\n  //\n  int32 word_color = 8;\n  //\n  int32 mission_page_show_type = 9;\n  //\n  string mission_url = 10;\n  //\n  string mission_text = 11;\n  //\n  TopicSet topic_set = 12;\n}\n\n//\nmessage FoldCardItem {\n  //\n  int32 is_show_fold = 1;\n  //\n  int64 fold_count = 2;\n  //\n  string card_show_desc = 3;\n  //\n  string fold_desc = 4;\n}\n\n//\nmessage FunctionalCard {\n  //\n  repeated TopicCapsule capsules = 1;\n  //\n  TrafficCard traffic_card = 2;\n  //\n  GameCard game_card = 3;\n}\n\n//\nmessage GameCard {\n  //\n  int64 game_id = 1;\n  //\n  string game_icon = 2;\n  //\n  string game_name = 3;\n  //\n  string score = 4;\n  //\n  string game_tags = 5;\n  //\n  string notice = 6;\n  //\n  string game_link = 7;\n}\n\n//\nmessage InlineProgressBar {\n  //\n  string icon_drag = 1;\n  //\n  string icon_drag_hash = 2;\n  //\n  string icon_stop = 3;\n  //\n  string icon_stop_hash = 4;\n}\n\n//\nmessage LargeCoverInline {\n  //\n  bilibili.app.card.v1.Base base = 1;\n  //\n  string cover_left_text1 = 2;\n  //\n  int32 cover_left_icon1 = 3;\n  //\n  string cover_left_text2 = 4;\n  //\n  int32 cover_left_icon2 = 5;\n  //\n  RightTopLiveBadge right_top_live_badge = 6;\n  //\n  string extra_uri = 7;\n  //\n  InlineProgressBar inline_progress_bar = 8;\n  //\n  TopicThreePoint topic_three_point = 9;\n  //\n  string cover_left_desc = 10;\n  //\n  bool hide_danmu_switch = 11;\n  //\n  bool disable_danmu = 12;\n  //\n  int32 can_play = 13;\n  //\n  string duration_text = 14;\n  //\n  RelationData relation_data = 15;\n}\n\n//\nmessage LiveBadgeResource {\n  //\n  string text = 1;\n  //\n  string animation_url = 2;\n  //\n  string animation_url_hash = 3;\n  //\n  string background_color_light = 4;\n  //\n  string background_color_night = 5;\n  //\n  int64 alpha_light = 6;\n  //\n  int64 alpha_night = 7;\n  //\n  string font_color = 8;\n}\n\n//\nmessage OperationCard {\n  oneof card {\n    //\n    LargeCoverInline large_cover_inline = 1;\n  }\n}\n\n//\nmessage OperationContent {\n  //\n  OperationCard operation_card = 1;\n}\n\n//\nmessage PubLayer {\n  //\n  int32 show_type = 1;\n  //\n  string jump_link = 2;\n  //\n  ButtonMeta button_meta = 3;\n  //\n  bool close_pub_layer_entry = 4;\n}\n\n//\nmessage RelationData {\n  //\n  bool is_fav = 1;\n  //\n  bool is_coin = 2;\n  //\n  bool is_follow = 3;\n  //\n  bool is_like = 4;\n  //\n  int64 like_count = 5;\n}\n\n//\nmessage RightTopLiveBadge {\n  //\n  int64 live_status = 1;\n  //\n  LiveBadgeResource in_live = 2;\n  //\n  string live_stats_desc = 3;\n}\n\n//\nmessage SortContent {\n  //\n  int64 sort_by = 1;\n  //\n  string sort_name = 2;\n}\n\n//\nmessage ThreePointItem {\n  //\n  string title = 1;\n  //\n  string jump_url = 2;\n}\n\n//\nmessage TimeLineEvents {\n  //\n  int64 event_id = 1;\n  //\n  string title = 2;\n  //\n  string time_desc = 3;\n  //\n  string jump_link = 4;\n}\n\n//\nmessage TimeLineResource {\n  //\n  int64 time_line_id = 1;\n  //\n  string time_line_title = 2;\n  //\n  repeated TimeLineEvents time_line_events = 3;\n  //\n  bool has_more = 4;\n}\n\n//\nmessage TopicActivities {\n  //\n  repeated TopicActivity activity = 1;\n  //\n  string act_list_title = 2;\n}\n\n//\nmessage TopicActivity {\n  //\n  int64 activity_id = 1;\n  //\n  string activity_name = 2;\n  //\n  string jump_url = 3;\n  //\n  string icon_url = 4;\n}\n\n//\nmessage TopicCapsule {\n  //\n  string name = 1;\n  //\n  string jump_url = 2;\n  //\n  string icon_url = 3;\n}\n\n//\nmessage TopicCardItem {\n  //\n  int32 type = 1;\n  //\n  bilibili.app.dynamic.v2.DynamicItem dynamic_item = 2;\n  //\n  FoldCardItem ford_card_item = 3;\n  //\n  VideoSmallCardItem video_small_card_item = 4;\n}\n\n//\nmessage TopicCardList {\n  //\n  repeated TopicCardItem topic_card_items = 1;\n  //\n  string offset = 2;\n  //\n  bool has_more = 3;\n  //\n  TopicSortByConf topic_sort_by_conf = 4;\n}\n\n//\nenum TopicCardType {\n  ILLEGAL_TYPE = 0;     //\n  DYNAMIC = 1;          //\n  FOLD = 2;             //\n  VIDEO_SMALL_CARD = 3; //\n}\n\n//\nmessage TopicDetailsAllReply {\n  //\n  DetailsTopInfo details_top_info = 1;\n  //\n  TopicActivities topic_activities = 2;\n  //\n  TopicCardList topic_card_list = 3;\n  //\n  FunctionalCard functional_card = 4;\n  //\n  PubLayer pub_layer = 5;\n  //\n  TimeLineResource time_line_resource = 6;\n  //\n  TopicServerConfig topic_server_config = 7;\n}\n\n//\nmessage TopicDetailsAllReq {\n  //\n  int64 topic_id = 1;\n  //\n  int64 sort_by = 2;\n  //\n  string offset = 3;\n  //\n  int32 page_size = 4;\n  //\n  int32 local_time = 5;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 6;\n  //\n  int32 need_refresh = 7;\n  //\n  string source = 8;\n  //\n  int32 topic_details_ext_mode = 9;\n}\n\n//\nenum TopicDetailsExtMode {\n  MODE_ILLEGAL_TYPE = 0;  //\n  STORY = 1;              //\n}\n\n//\nmessage TopicDetailsFoldReply {\n  //\n  TopicCardList topic_card_list = 1;\n  //\n  int64 fold_count = 2;\n}\n\n//\nmessage TopicDetailsFoldReq {\n  //\n  int64 topic_id = 1;\n  //\n  string offset = 2;\n  //\n  int32 page_size = 3;\n  //\n  int32 local_time = 4;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 5;\n  //\n  int64 from_sort_by = 6;\n}\n\n//\nmessage TopicInfo {\n  //\n  int64 id = 1;\n  //\n  string name = 2;\n  //\n  int64 uid = 3;\n  //\n  int64 view = 4;\n  //\n  int64 discuss = 5;\n  //\n  int64 fav = 6;\n  //\n  int64 dynamics = 7;\n  //\n  int32 state = 8;\n  //\n  string jump_url = 9;\n  //\n  string backcolor = 10;\n  //\n  bool is_fav = 11;\n  //\n  string description = 12;\n  //\n  int32 create_source = 13;\n  //\n  string share_pic = 14;\n  //\n  int64 share = 15;\n  //\n  int64 like = 16;\n  //\n  string share_url = 17;\n  //\n  bool is_like = 18;\n  //\n  int32 type = 19;\n  //\n  string stats_desc = 20;\n  //\n  string fixed_topic_icon = 21;\n}\n\n//\nmessage TopicServerConfig {\n  //\n  int64 pub_events_increase_threshold = 1;\n  //\n  int64 pub_events_hidden_timeout_threshold = 2;\n  //\n  int64 vert_online_refresh_time = 3;\n}\n\n//\nmessage TopicSet {\n  //\n  int64 set_id = 1;\n  //\n  string set_name = 2;\n  //\n  string jump_url = 3;\n  //\n  string desc = 4;\n}\n\n//\nmessage TopicSetDetailsReply {\n  //\n  TopicSetHeadInfo topic_set_head_info = 1;\n  //\n  repeated TopicInfo topic_info = 2;\n  //\n  bool has_more = 3;\n  //\n  string offset = 4;\n  //\n  TopicSetSortCfg sort_cfg = 5;\n}\n\n//\nmessage TopicSetDetailsReq {\n  //\n  int64 set_id = 1;\n  //\n  int64 sort_by = 2;\n  //\n  string offset = 3;\n  //\n  int32 page_size = 4;\n}\n\nmessage TopicSetHeadInfo {\n  //\n  TopicSet topic_set = 1;\n  //\n  string topic_cnt_text = 2;\n  //\n  string head_img_url = 3;\n  //\n  string mission_url = 4;\n  //\n  string mission_text = 5;\n  //\n  string icon_url = 6;\n  //\n  bool is_fav = 7;\n  //\n  bool is_first_time = 8;\n}\n\n//\nmessage TopicSetSortCfg {\n  //\n  int64 default_sort_by = 1;\n  //\n  repeated SortContent all_sort_by = 2;\n}\n\n//\nmessage TopicSortByConf {\n  //\n  int64 default_sort_by = 1;\n  //\n  repeated SortContent all_sort_by = 2;\n  //\n  int64 show_sort_by = 3;\n}\n\n//\nmessage TopicThreePoint {\n  //\n  repeated ThreePointItem dyn_three_point_items = 1;\n}\n\n//\nmessage TrafficCard {\n  //\n  string name = 1;\n  //\n  string jump_url = 2;\n  //\n  string icon_url = 3;\n  //\n  string base_pic = 4;\n  //\n  string benefit_point = 5;\n  //\n  string card_desc = 6;\n  //\n  string jump_title = 7;\n}\n\n//\nmessage User {\n  //\n  int64 uid = 1;\n  //\n  string face = 2;\n  //\n  string name = 3;\n  //\n  string name_desc = 4;\n}\n\n//\nmessage VideoCardBase {\n  //\n  string cover = 1;\n  //\n  string title = 2;\n  //\n  string up_name = 3;\n  //\n  int64 play = 4;\n  //\n  string jump_link = 5;\n  //\n  int64 aid = 6;\n}\n\n//\nmessage VideoSmallCardItem {\n  //\n  VideoCardBase video_card_base = 1;\n  //\n  string cover_left_badge_text = 2;\n  //\n  int64 card_stat_icon1 = 3;\n  //\n  string card_stat_text1 = 4;\n  //\n  int64 card_stat_icon2 = 5;\n  //\n  string card_stat_text2 = 6;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/view/v1/view.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.view.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/any.proto\";\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\nimport \"bilibili/app/archive/v1/archive.proto\";\nimport \"bilibili/pagination/pagination.proto\";\nimport \"bilibili/app/viewunite/common.proto\";\n\nservice View {\n  // 视频页详情页\n  rpc View (ViewReq) returns (ViewReply);\n  //\n  rpc ViewTag(ViewTagReq) returns (ViewTagReply);\n  //\n  rpc ViewMaterial(ViewMaterialReq) returns (ViewMaterialReply);\n  // 视频播放过程中的数据\n  rpc ViewProgress (ViewProgressReq) returns (ViewProgressReply);\n  // 短视频下载\n  rpc ShortFormVideoDownload (ShortFormVideoDownloadReq) returns (ShortFormVideoDownloadReply);\n  // 点击播放器卡片事件\n  rpc ClickPlayerCard (ClickPlayerCardReq) returns (NoReply);\n  // 点击大型活动页预约\n  rpc ClickActivitySeason (ClickActivitySeasonReq) returns (NoReply);\n  // 合集详情页\n  rpc Season (SeasonReq) returns (SeasonReply);\n  // 播放器卡片曝光\n  rpc ExposePlayerCard (ExposePlayerCardReq) returns (NoReply);\n  // 点击签订契约\n  rpc AddContract (AddContractReq) returns (NoReply);\n  // 资源包\n  rpc ChronosPkg(ChronosPkgReq) returns (Chronos);\n  //\n  rpc CacheView(CacheViewReq) returns (CacheViewReply);\n  //\n  rpc ContinuousPlay(ContinuousPlayReq) returns (ContinuousPlayReply);\n  // 播放页推荐IFS\n  rpc RelatesFeed(RelatesFeedReq) returns (RelatesFeedReply);\n  //\n  rpc PremiereArchive(PremiereArchiveReq) returns (PremiereArchiveReply);\n  //\n  rpc Reserve(ReserveReq) returns (ReserveReply);\n  //\n  rpc PlayerRelates(PlayerRelatesReq) returns (PlayerRelatesReply);\n  //\n  rpc SeasonActivityRecord(SeasonActivityRecordReq) returns (SeasonActivityRecordReply);\n  //\n  rpc SeasonWidgetExpose(SeasonWidgetExposeReq) returns (SeasonWidgetExposeReply);\n  //\n  rpc GetArcsPlayer(GetArcsPlayerReq) returns (GetArcsPlayerReply);\n}\n\n// 活动页资源包\nmessage ActivityResource {\n  // mod资源池名称\n  string mod_pool_name = 1;\n  // mod资源名称\n  string mod_resource_name = 2;\n  // 背景色\n  string bg_color = 3;\n  // 选中背景色\n  string selected_bg_color = 4;\n  // 文字颜色\n  string text_color = 5;\n  // 浅字色\n  string light_text_color = 6;\n  // 深字色\n  string dark_text_color = 7;\n  // 分割线色\n  string divider_color = 8;\n}\n\n// 大型活动合集\nmessage ActivitySeason {\n  // 稿件信息\n  bilibili.app.archive.v1.Arc arc = 1;\n  // 分P信息\n  repeated ViewPage pages = 2;\n  //(\"OnwerExt\"为源码中拼写错误)\n  OnwerExt owner_ext = 3;\n  // 稿件用户操作状态\n  ReqUser req_user = 4;\n  // 充电排行\n  ElecRank elec_rank = 5;\n  // 历史观看进度\n  History history = 6;\n  // 稿件bvid\n  string bvid = 7;\n  // 获得荣誉信息\n  Honor honor = 8;\n  // 联合投稿成员列表\n  repeated Staff staff = 9;\n  // UGC视频合集信息\n  UgcSeason ugc_season = 10;\n  // 播放页定制tab\n  Tab tab = 11;\n  // 排行榜\n  Rank rank = 12;\n  // 预约模块\n  Order order = 13;\n  // 是否支持点踩\n  bool support_dislike = 14;\n  // 相关推荐(运营配置+AI推荐)\n  OperationRelate operation_relate = 15;\n  // 活动页资源包\n  ActivityResource activity_resource = 16;\n  // 短链接\n  string short_link = 17;\n  // 标签\n  Label label = 18;\n  // 不感兴趣原因\n  Dislike dislike = 19;\n  // 播放图标动画配置档\n  PlayerIcon player_icon = 20;\n  // 分享副标题(已观看xxx次)\n  string share_subtitle = 21;\n  // 广告配置\n  CMConfig cm_config = 22;\n  // 免流面板定制\n  TFPanelCustomized tf_panel_customized = 23;\n  // 争议信息\n  string argue_msg = 24;\n  // 错误码\n  // DEFAULT:正常 CODE404:视频被UP主删除\n  ECode ecode = 25;\n  // 404页信息\n  CustomConfig custom_config = 26;\n  // 评论样式\n  string badge_url = 27;\n  // 稿件简介v2\n  repeated DescV2 desc_v2 = 28;\n  //\n  Config config = 29;\n  //\n  Online online = 30;\n  //\n  ArcExtra arc_extra = 31;\n  //\n  ReplyStyle reply_preface = 32;\n}\n\n// 点击签订契约-请求\nmessage AddContractReq {\n  // 稿件avid\n  int64 aid = 1;\n  // UP主mid\n  int64 up_mid = 2;\n  // 当前页面spm\n  string spmid = 3;\n}\n\n//\nmessage AdInfo {\n  //\n  int64 creative_id = 1;\n  //\n  int64 creative_type = 2;\n  //\n  CreativeContent creative_content = 3;\n  //\n  string ad_cb = 4;\n  //\n  int32 card_type = 5;\n  //\n  bytes extra = 6;\n}\n\n//\nmessage ArcExtra {\n  //\n  string arc_pub_location = 1;\n}\n\n//\nmessage ArcsPlayer {\n  //\n  int64 aid = 1;\n  //\n  map<int64, string> player_info = 2;\n}\n\n//\nmessage Asset {\n  //\n  int32 paid = 1;\n  //\n  int64 price = 2;\n  //\n  AssetMsg msg = 3;\n  //\n  AssetMsg preview_msg = 4;\n}\n\n//\nmessage AssetMsg {\n  //\n  string desc1 = 1;\n  //\n  string desc2 = 2;\n}\n\n// 关注按钮卡片\nmessage Attention {\n  // 开始时间\n  int32 start_time = 1;\n  // 结束时间\n  int32 end_time = 2;\n  // 位置x坐标\n  double pos_x = 3;\n  // 位置y坐标\n  double pos_y = 4;\n}\n\n// 音频稿件信息\nmessage Audio {\n  // 音频标题\n  string title = 1;\n  // 音频封面url\n  string cover_url = 2;\n  // 音频auid\n  int64 song_id = 3;\n  // 音频播放量\n  int64 play_count = 4;\n  // 音频评论数\n  int64 reply_count = 5;\n  // 音频作者UID\n  int64 upper_id = 6;\n  // 进入按钮文案\n  string entrance = 7;\n  //\n  int64 song_attr = 8;\n}\n\n//\nmessage BadgeStyle {\n  //\n  string text = 1;\n  //\n  string text_color = 2;\n  //\n  string text_color_night = 3;\n  //\n  string bg_color = 4;\n  //\n  string bg_color_night = 5;\n  //\n  string border_color = 6;\n  //\n  string border_color_night = 7;\n  //\n  int32 bg_style = 8;\n}\n\n// 视频引用的bgm音频\nmessage Bgm {\n  // 音频auid\n  int64 sid = 1;\n  // 音频作者mid\n  int64 mid = 2;\n  // 音频标题\n  string title = 3;\n  // 音频作者昵称\n  string author = 4;\n  // bgm页面url\n  string jumpUrl = 5;\n  // 音频封面url\n  string cover = 6;\n}\n\n// 收藏合集参数\nmessage BizFavSeasonParam {\n  // 合集id\n  int64 season_id = 1;\n}\n\n//\nmessage BizFollowVideoParam {\n  //\n  int64 season_id = 1;\n}\n\n//\nmessage BizJumpLinkParam {\n  // 链接\n  string url = 1;\n}\n\n// 预约活动参数\nmessage BizReserveActivityParam {\n  // 活动id\n  int64 activity_id = 1;\n  // 场景\n  string from = 2;\n  // 类型\n  string type = 3;\n  // 资源id\n  int64 oid = 4;\n  //\n  int64 reserve_id = 5;\n}\n\n//\nmessage BizReserveGameParam {\n  // 游戏id\n  int64 game_id = 1;\n}\n\n// 业务类型\nenum BizType {\n  BizTypeNone = 0; //\n  BizTypeFollowVideo = 1; // 追番追剧\n  BizTypeReserveActivity = 2; // 预约活动\n  BizTypeJumpLink = 3; // 跳转链接\n  BizTypeFavSeason = 4; // 收藏合集\n  BizTypeReserveGame = 5; // 预约游戏\n}\n\n//\nmessage Button {\n  // 按钮文案\n  string title = 1;\n  // 跳转uri\n  string uri = 2;\n  //\n  string icon = 3;\n}\n\n//\nmessage ButtonStyle {\n  //\n  string text = 1;\n  //\n  string text_color = 2;\n  //\n  string text_color_night = 3;\n  //\n  string bg_color = 4;\n  //\n  string bg_color_night = 5;\n  //\n  string jump_link = 6;\n}\n\n//\nmessage BuzzwordConfig {\n  //\n  string name = 1;\n  //\n  string schema = 2;\n  //\n  int32 source = 3;\n  //\n  int64 start = 4;\n  //\n  int64 end = 5;\n  //\n  bool follow_control = 6;\n  //\n  int64 id = 7;\n  //\n  int64 buzzword_id = 8;\n  //\n  int32 schema_type = 9;\n  //\n  string picture = 10;\n}\n\n//\nmessage CacheViewReply {\n  //\n  bilibili.app.archive.v1.Arc arc = 1;\n  //\n  repeated ViewPage pages = 2;\n  //\n  OnwerExt owner_ext = 3;\n  //\n  ReqUser req_user = 4;\n  //\n  Season season = 5;\n  //\n  ElecRank elec_rank = 6;\n  //\n  History history = 7;\n  //\n  Dislike dislike = 8;\n  //\n  PlayerIcon player_icon = 9;\n  //\n  string bvid = 10;\n  //\n  string short_link = 11;\n  //\n  string share_subtitle = 12;\n  //\n  TFPanelCustomized tf_panel_customized = 13;\n  //\n  Online online = 14;\n}\n\n//\nmessage CacheViewReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n  //\n  string from = 3;\n  //\n  string trackid = 4;\n  //\n  string ad_extra = 5;\n  //\n  string spmid = 6;\n  //\n  string from_spmid = 7;\n}\n\n//\nenum Category {\n  CategoryUnknown = 0; //\n  CategorySeason = 1; //\n}\n\n// Chronos灰度管理\nmessage Chronos {\n  // 资源包md5\n  string md5 = 1;\n  // 资源包\n  string file = 2;\n  //\n  string sign = 3;\n}\n\n//\nmessage ChronosPkgReq {\n  //\n  string service_key = 1;\n  //\n  string engine_version = 2;\n  //\n  string message_protocol = 3;\n}\n\n// 点击大型活动页预约-请求\nmessage ClickActivitySeasonReq {\n  // 预约类型\n  BizType order_type = 1;\n  // 当前页面spm\n  string spmid = 2;\n  // 业务参数\n  oneof order_param {\n    // 预约活动参数\n    BizReserveActivityParam reserve = 3;\n    // 收藏合集参数\n    BizFavSeasonParam fav_season = 4;\n  }\n  // 操作\n  // 0:操作 1:取消操作\n  int64 action = 5;\n}\n\n// 点击播放器卡片-响应\nmessage ClickPlayerCardReply {\n  //\n  string message = 1;\n}\n\n// 点击播放器卡片-请求\nmessage ClickPlayerCardReq {\n  // 卡片id\n  int64 id = 1;\n  // 稿件avid\n  int64 aid = 2;\n  // 视频cid\n  int64 cid = 3;\n  //操作\n  //0:操作 1:取消操作\n  int64 action = 4;\n  // 当前页面spm\n  string spmid = 5;\n}\n\n// 广告\nmessage CM {\n  // 广告数据(需解包)\n  google.protobuf.Any source_content = 1;\n}\n\n// 广告配置\nmessage CMConfig {\n  // 广告配置数据(需要二次解包)\n  google.protobuf.Any ads_control = 1;\n}\n\n//\nmessage CmIpad {\n  //\n  CM cm = 1;\n  //\n  bilibili.app.archive.v1.Author author = 2;\n  //\n  bilibili.app.archive.v1.Stat stat = 3;\n  //\n  int64 duration = 4;\n  //\n  int64 aid = 5;\n}\n\n//\nmessage CoinCustom {\n  //\n  string toast = 1;\n}\n\n// 互动弹幕条目信息\nmessage CommandDm {\n  // 弹幕id\n  int64 id = 1;\n  // 对象视频cid\n  int64 oid = 2;\n  // 发送者mid\n  int64 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 id_str = 10;\n}\n\n//\nmessage Config {\n  // 下方推荐项标题\n  string relates_title = 1;\n  //\n  int32 relates_style = 2;\n  //\n  int32 relate_gif_exp = 3;\n  //\n  int32 end_page_half = 4;\n  //\n  int32 end_page_full = 5;\n  // 退出是否自动小窗\n  bool auto_swindow = 6;\n  //\n  bool popup_info = 7;\n  //\n  string abtest_small_window = 8;\n  //\n  int32 rec_three_point_style = 9;\n  //\n  bool is_absolute_time = 10;\n  //\n  bool new_swindow = 11;\n  //\n  bool relates_biserial = 12;\n  //\n  ListenerConfig listener_conf = 13;\n  //\n  string relates_feed_style = 14;\n  //\n  bool relates_feed_popup = 15;\n  //\n  bool relates_has_next = 16;\n  //\n  int32 local_play = 17;\n  //\n  bool play_story = 18;\n  //\n  bool arc_play_story = 19;\n  //\n  string story_icon = 20;\n  //\n  bool landscape_story = 21;\n  //\n  bool arc_landscape_story = 22;\n  //\n  string landscape_icon = 23;\n  //\n  bool show_listen_button = 24;\n}\n\n//\nmessage ContinuousPlayReply {\n  //\n  repeated Relate relates = 1;\n}\n\n//\nmessage ContinuousPlayReq {\n  //\n  int64 aid = 1;\n  //\n  string from = 2;\n  //\n  string trackid = 3;\n  //\n  string spmid = 4;\n  //\n  string from_spmid = 5;\n  //\n  int32 autoplay = 6;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 7;\n  //\n  int64 device_type = 8;\n  //\n  string session_id = 9;\n  //\n  int64 display_id = 10;\n}\n\n// 契约卡\nmessage ContractCard {\n  // 需要触发的播放进度百分比\n  float display_progress = 1;\n  // 触发位置的前后误差(单位ms)\n  int64 display_accuracy = 2;\n  // 展示持续时间(单位ms)\n  int64 display_duration = 3;\n  // 弹出模式\n  // 0: 原有模式 1: 半屏弹出 2: 全屏、半屏均弹出\n  int32 show_mode = 4;\n  // 提示页面\n  // 0: 原有页面 1: 6.23版本新页面\n  int32 page_type = 5;\n  // UP主信息\n  UpperInfos upper = 6;\n  //\n  int32 is_follow_display = 7;\n  //\n  ContractText text = 8;\n  //\n  int64 follow_display_end_duration = 9;\n  //\n  int32 is_play_display = 10;\n  //\n  int32 is_interact_display = 11;\n  //\n  bool play_display_switch = 12;\n}\n\n//\nmessage ContractText {\n  //\n  string title = 1;\n  //\n  string subtitle = 2;\n  //\n  string inline_title = 3;\n}\n\n//\nmessage CreativeContent {\n  //\n  string title = 1;\n  //\n  string description = 2;\n  //\n  string button_title = 3;\n  //\n  int64 video_id = 4;\n  //\n  string username = 5;\n  //\n  string image_url = 6;\n  //\n  string image_md5 = 7;\n  //\n  string log_url = 8;\n  //\n  string log_md5 = 9;\n  //\n  string url = 10;\n  //\n  string click_url = 11;\n  //\n  string show_url = 12;\n}\n\n// 404页信息\nmessage CustomConfig {\n  // 重定向页面url\n  string redirect_url = 1;\n}\n\n// 枚举-文本类型\nenum DescType {\n  DescTypeUnknown = 0; // 占位\n  DescTypeText = 1; // 文本\n  DescTypeAt = 2; // @\n}\n\n// 特殊稿件简介\nmessage DescV2 {\n  // 文本内容\n  string text = 1;\n  // 文本类型\n  DescType type = 2;\n  // 点击跳转链接\n  string uri = 3;\n  // 资源ID\n  int64 rid = 4;\n}\n\n// 不喜欢原因\nmessage Dislike {\n  // 标题\n  string title = 1;\n  //\n  string subtitle = 2;\n  // 原因项列表\n  repeated DislikeReasons reasons = 3;\n}\n\n// 不喜欢原因项\nmessage DislikeReasons {\n  // 类型\n  // 1:全部类型 3:TAG 4:UP主\n  int64 id = 1;\n  // 相关UP主mid\n  int64 mid = 2;\n  // 相关分区tid\n  int32 rid = 3;\n  // 相关TAG id\n  int64 tag_id = 4;\n  // 相关名称\n  string name = 5;\n}\n\n// 分P弹幕信息\nmessage DM {\n  // 分P是否关闭弹幕\n  // 0:正常 1:关闭\n  bool closed = 1;\n  //\n  bool real_name = 2;\n  // 分P弹幕总数\n  int64 count = 3;\n}\n\n// 错误代码\nenum ECode {\n  DEFAULT = 0; // 正常\n  CODE404 = 1; // 稿件被UP主删除\n}\n\n// 充电排行信息\nmessage ElecRank {\n  // 充电排行列表\n  repeated ElecRankItem list = 1;\n  // 充电用户数\n  int64 count = 2;\n  //\n  string text = 3;\n}\n\n// 充电用户信息\nmessage ElecRankItem {\n  // 用户头像url\n  string avatar = 1;\n  // 用户昵称\n  string nickname = 2;\n  // 充电留言\n  string message = 3;\n  // 用户mid\n  int64 mid = 4;\n}\n\n// 视频合集单话信息\nmessage Episode {\n  // 合集单话id\n  int64 id = 1;\n  // 稿件avid\n  int64 aid = 2;\n  // 视频1P cid\n  int64 cid = 3;\n  // 稿件标题\n  string title = 4;\n  // 稿件封面url\n  string cover = 5;\n  // 投稿时间显示文案\n  string coverRightText = 6;\n  // 视频分P信息\n  bilibili.app.archive.v1.Page page = 7;\n  // 视频状态数\n  bilibili.app.archive.v1.Stat stat = 8;\n  // 稿件bvid\n  string bvid = 9;\n  // 稿件UP主信息\n  bilibili.app.archive.v1.Author author = 10;\n  //\n  string author_desc = 11;\n  //\n  BadgeStyle badge_style = 12;\n  //\n  bool need_pay = 13;\n  //\n  bool episode_pay = 14;\n  //\n  bool free_watch = 15;\n  //\n  string first_frame = 16;\n  //\n  ArchiveStat stat_v2 = 17;\n  //\n  repeated bilibili.app.archive.v1.Page pages = 18;\n}\n\n//\nmessage ArchiveStat {\n  //\n  bilibili.app.viewunite.common.StatInfo view_vt = 11;\n}\n\n// 播放器卡片曝光-请求\nmessage ExposePlayerCardReq {\n  // 卡片类型\n  PlayerCardType card_type = 1;\n  // 稿件avid\n  int64 aid = 2;\n  // 视频cid\n  int64 cid = 3;\n  // 当前页面spm\n  string spmid = 4;\n}\n\n//\nmessage FeedViewItem {\n  //\n  ViewReply view = 1;\n  //\n  string goto = 2;\n  //\n  string uri = 3;\n  //\n  string track_id = 4;\n}\n\n//\nmessage FeedViewReply {\n  //\n  repeated FeedViewItem list = 1;\n  //\n  bool has_next = 2;\n}\n\n//\nmessage FeedViewReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n  //\n  string from = 3;\n  //\n  string spmid = 4;\n  //\n  string from_spmid = 5;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 6;\n  //\n  int64 display_id = 7;\n  //\n  string session_id = 8;\n  //\n  string page_version = 9;\n  //\n  string from_track_id = 10;\n}\n\n//\nmessage GetArcsPlayerReply {\n  //\n  repeated ArcsPlayer arcs_player = 1;\n}\n\n//\nmessage GetArcsPlayerReq {\n  //\n  repeated PlayAv play_avs = 1;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 2;\n}\n\n//\nmessage GoodsInfo {\n  //\n  string goods_id = 1;\n  //\n  int32 category = 2;\n  //\n  int64 goods_price = 3;\n  //\n  PayState pay_state = 4;\n  //\n  string goods_name = 5;\n  //\n  string price_fmt = 6;\n}\n\n// 稿件观看进度\nmessage History {\n  // 播放进度分P cid\n  int64 cid = 1;\n  // 播放进度时间\n  // 0:未观看 -1:已看完 正整数:播放时间进度\n  int64 progress = 2;\n}\n\n// 稿件获得荣誉信息\nmessage Honor {\n  // 荣誉栏图标url\n  string icon = 1;\n  // 荣誉栏图标url 夜间模式\n  string icon_night = 2;\n  // 荣誉文案\n  string text = 3;\n  // 荣誉副文案\n  string text_extra = 4;\n  // 标题颜色\n  string text_color = 5;\n  // 标题颜色 夜间模式\n  string text_color_night = 6;\n  // 背景颜色\n  string bg_color = 7;\n  // 背景颜色 夜间模式\n  string bg_color_night = 8;\n  // 跳转uri\n  string url = 9;\n  // 跳转角标文案\n  string url_text = 10;\n}\n\n//\nmessage IconData {\n  //\n  string meta_json = 1;\n  //\n  string sprits_img = 2;\n}\n\n//\nmessage Interaction {\n  //\n  Node history_node = 1;\n  //\n  int64 graph_version = 2;\n  //\n  string msg = 3;\n  //\n  string evaluation = 4;\n  //\n  int64 mark = 5;\n}\n\n//\nmessage Label {\n  //\n  int32 type = 1;\n  //\n  string uri = 2;\n  //\n  string icon = 3;\n  //\n  string icon_night = 4;\n  //\n  int64 icon_width = 5;\n  //\n  int64 icon_height = 6;\n  //\n  string lottie = 7;\n  //\n  string lottie_night = 8;\n}\n\n//\nmessage LikeAnimation {\n  //\n  string like_icon = 1;\n  //\n  string liked_icon = 2;\n  //\n  string like_animation = 3;\n}\n\n//\nmessage LikeCustom {\n  //\n  bool like_switch = 1;\n  //\n  int64 full_to_half_progress = 2;\n  //\n  int64 non_full_progress = 3;\n  //\n  int64 update_count = 4;\n}\n\n//\nmessage ListenerConfig {\n  //\n  int64 jump_style = 1;\n  //\n  ListenerGuideBar guide_bar = 2;\n}\n\n//\nmessage ListenerGuideBar {\n  //\n  int64 show_strategy = 1;\n  //\n  string icon = 2;\n  //\n  string text = 3;\n  //\n  string btn_text = 4;\n  //\n  int64 show_time = 5;\n  //\n  int64 background_time = 6;\n}\n\n// 直播信息\nmessage Live {\n  // 主播UID\n  int64 mid = 1;\n  // 直播间id\n  int64 roomid = 2;\n  // 直播间url\n  string uri = 3;\n  //\n  string endpage_uri = 4;\n}\n\n// 直播预约信息\nmessage LiveOrderInfo {\n  // 预约id\n  int64 sid = 1;\n  // 预约条文案\n  string text = 2;\n  // 直播开始时间\n  int64 live_plan_start_time = 3;\n  // 是否预约\n  bool is_follow = 4;\n}\n\n//\nmessage MaterialLeft {\n  //\n  string icon = 1;\n  //\n  string text = 2;\n  //\n  string url = 3;\n  //\n  string left_type = 4;\n  //\n  string param = 5;\n  //\n  string operational_type = 6;\n  //\n  string static_icon = 7;\n}\n\n//\nmessage MaterialRes {\n  //\n  int64 id = 1;\n  //\n  string icon = 2;\n  //\n  string url = 3;\n  //\n  MaterialSource type = 4;\n  //\n  string name = 5;\n  //\n  string bg_color = 6;\n  //\n  string bg_pic = 7;\n  //\n  int32 jump_type = 8;\n}\n\n//\nenum MaterialSource {\n  Default = 0; //\n  BiJian = 1; // 必剪\n}\n\n//\nmessage Node {\n  //\n  int64 node_id = 1;\n  //\n  string title = 2;\n  //\n  int64 cid = 3;\n}\n\n// 空回复\nmessage NoReply {}\n\n//\nmessage Notice {\n  //\n  string title = 1;\n  //\n  string desc = 2;\n}\n\n// 认证信息\nmessage OfficialVerify {\n  // 认证类型\n  // 0:个人认证 1:官方认证\n  int32 type = 1;\n  //认证名称\n  string desc = 2;\n}\n\n//\nmessage Online {\n  //\n  bool online_show = 1;\n  //\n  string player_online_logo = 2;\n}\n\n// UP主扩展信息 (\"OnwerExt\"为源码中拼写错误)\nmessage OnwerExt {\n  // 认证信息\n  OfficialVerify official_verify = 1;\n  // 直播信息\n  Live live = 2;\n  // 会员信息\n  Vip vip = 3;\n  //\n  repeated int64 assists = 4;\n  // 粉丝数\n  int64 fans = 5;\n  // 总投稿数\n  string arc_count = 6;\n}\n\n// 老运营卡片\nmessage OperationCard {\n  // 开始时间(单位为秒)\n  int32 start_time = 1;\n  // 结束时间(单位为秒)\n  int32 end_time = 2;\n  // 图标\n  string icon = 3;\n  // 标题\n  string title = 4;\n  // 按钮文案\n  string button_text = 5;\n  // 跳转链接\n  string url = 6;\n  // 内容描述\n  string content = 7;\n}\n\n// 内嵌操作按钮卡片\nmessage OperationCardNew {\n  // 卡片id\n  int64 id = 1;\n  // 开始时间\n  int32 from = 2;\n  // 结束时间\n  int32 to = 3;\n  // 用户操作态\n  // true已操作 false未操作\n  bool status = 4;\n  // 卡片类型\n  OperationCardType card_type = 5;\n  // 卡片渲染\n  oneof render {\n    // 标准卡\n    StandardCard standard = 6;\n    // 老运营卡片(原B剪跳转卡)\n    OperationCard skip = 7;\n  }\n  //\n  BizType biz_type = 8;\n  //\n  oneof param {\n    // 追番追剧参数\n    BizFollowVideoParam follow = 9;\n    // 预约活动参数\n    BizReserveActivityParam reserve = 10;\n    // 跳转参数\n    BizJumpLinkParam jump = 11;\n    // 预约游戏参数\n    BizReserveGameParam game = 12;\n  }\n}\n\n// 卡片样式\nenum OperationCardType {\n  CardTypeNone = 0; //\n  CardTypeStandard = 1; // 标准卡\n  CardTypeSkip = 2; // 原跳转卡\n}\n\n//\nmessage OperationCardV2 {\n  //\n  int64 id = 1;\n  //\n  int32 from = 2;\n  //\n  int32 to = 3;\n  //\n  bool status = 4;\n  //\n  int32 biz_type = 5;\n  //\n  OperationCardV2Content content = 6;\n  //\n  oneof param {\n    //\n    BizFollowVideoParam BizFollowVideoParam = 7;\n    //\n    BizReserveActivityParam BizReserveActivityParam = 8;\n    //\n    BizJumpLinkParam BizJumpLinkParam = 9;\n    //\n    BizReserveGameParam BizReserveGameParam = 10;\n  }\n}\n\n//\nmessage OperationCardV2Content {\n  //\n  string title = 1;\n  //\n  string subtitle = 2;\n  //\n  string icon = 3;\n  //\n  string button_title = 4;\n  //\n  string button_selected_title = 5;\n  //\n  bool show_selected = 6;\n}\n\n// 相关推荐(运营配置+AI推荐)\nmessage OperationRelate {\n  // 模块标题\n  string title = 1;\n  // 相关推荐模块内容\n  repeated RelateItem relate_item = 2;\n  // AI相关推荐\n  repeated Relate ai_relate_item = 3;\n}\n\n// 预约模块\nmessage Order {\n  // 用户操作态\n  bool status = 1;\n  // 模块标题\n  string title = 2;\n  // 按钮文字 未操作\n  string button_title = 3;\n  // 按钮文字 已操作\n  string button_selected_title = 4;\n  // 合集播放数\n  int64 season_stat_view = 5;\n  // 合集弹幕数\n  int64 season_stat_danmaku = 6;\n  // 预约类型(点击时透传，直播开始前预约活动，直播开始后收藏合集)\n  BizType order_type = 7;\n  // 预约业务参数\n  oneof order_param {\n    // 预约活动参数\n    BizReserveActivityParam reserve = 8;\n    // 收藏合集参数\n    BizFavSeasonParam fav_season = 9;\n  }\n  // 合集简介\n  string intro = 10;\n}\n\n// 游戏礼包信息\nmessage PackInfo {\n  // 礼包标题\n  string title = 1;\n  // 礼包页uri\n  string uri = 2;\n}\n\n//\nenum PayState {\n  PayStateUnknown = 0; //\n  PayStateActive = 1; //\n}\n\n//\nmessage PlayAv {\n  //\n  int64 aid = 1;\n  //\n  int64 cid = 2;\n}\n\n// 卡片类型\nenum PlayerCardType {\n  PlayerCardTypeNone_VALUE = 0; //\n  PlayerCardTypeAttention_VALUE = 1; // 关注卡\n  PlayerCardTypeOperation_VALUE = 2; // 运营卡\n  PlayerCardTypeContract_VALUE = 3; // 契约卡\n}\n\n// 进度条动画配置\nmessage PlayerIcon {\n  // 拖动动画配置档url\n  string url1 = 1;\n  // 拖动动画配置档hash\n  string hash1 = 2;\n  // 松手动画配置档url\n  string url2 = 3;\n  // 松手动画配置档hash\n  string hash2 = 4;\n  //\n  string drag_left_png = 5;\n  //\n  string middle_png = 6;\n  //\n  string drag_right_png = 7;\n  //\n  IconData drag_data = 8;\n  //\n  IconData nodrag_data = 9;\n}\n\n//\nmessage PlayerRelatesReply {\n  //\n  repeated Relate list = 1;\n}\n\n//\nmessage PlayerRelatesReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n  //\n  string from = 3;\n  //\n  string spmid = 4;\n  //\n  string from_spmid = 5;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 6;\n  //\n  string session_id = 7;\n  //\n  string from_track_id = 8;\n}\n\n//\nmessage PointMaterial {\n  //\n  string url = 1;\n  //\n  int32 material_source = 2;\n}\n\n//\nmessage PowerIconStyle {\n  //\n  string icon_url = 1;\n  //\n  string icon_night_url = 2;\n  //\n  int64 icon_width = 3;\n  //\n  int64 icon_height = 4;\n}\n\n//\nmessage Premiere {\n  //\n  PremiereState premiere_state = 1;\n  //\n  int64 start_time = 2;\n  //\n  int64 service_time = 3;\n  //\n  int64 room_id = 4;\n}\n\n//\nmessage PremiereArchiveReply {\n  //\n  Premiere premiere = 1;\n  //\n  bool risk_status = 2;\n  //\n  string risk_reason = 3;\n}\n\n//\nmessage PremiereArchiveReq {\n  //\n  int64 aid = 1;\n}\n\n//\nmessage PremiereReserve {\n  //\n  int64 reserve_id = 1;\n  //\n  int64 count = 2;\n  //\n  bool is_follow = 3;\n}\n\n//\nmessage PremiereResource {\n  //\n  Premiere premiere = 1;\n  //\n  PremiereReserve reserve = 2;\n  //\n  PremiereText text = 3;\n}\n\n//\nenum PremiereState {\n  premiere_none = 0; //\n  premiere_before = 1; //\n  premiere_in = 2; //\n  premiere_after = 3; //\n}\n\n//\nmessage PremiereText {\n  //\n  string title = 1;\n  //\n  string subtitle = 2;\n  //\n  string online_text = 3;\n  //\n  string online_icon = 4;\n  //\n  string online_icon_dark = 5;\n  //\n  string intro_title = 6;\n  //\n  string intro_icon = 7;\n  //\n  string guidance_pulldown = 8;\n  //\n  string guidance_entry = 9;\n  //\n  string intro_icon_night = 10;\n}\n\n//\nmessage PullClientAction {\n  //\n  string type = 1;\n  //\n  bool pull_action = 2;\n  //\n  string params = 3;\n}\n\n// 排行榜\nmessage Rank {\n  // 排行榜icon\n  string icon = 1;\n  // 排行榜icon 夜间模式\n  string icon_night = 2;\n  // 排行榜文案\n  string text = 3;\n}\n\n//\nmessage RankInfo {\n  //\n  string icon_url_night = 1;\n  //\n  string icon_url_day = 2;\n  //\n  string bkg_night_color = 3;\n  //\n  string bkg_day_color = 4;\n  //\n  string font_night_color = 5;\n  //\n  string font_day_color = 6;\n  //\n  string rank_content = 7;\n  //\n  string rank_link = 8;\n}\n\n// 推荐理由样式\nmessage ReasonStyle {\n  //\n  string text = 1;\n  // 日间模式文字\n  string text_color = 2;\n  //\n  string bg_color = 3;\n  //\n  string border_color = 4;\n  // 夜间模式文字\n  string text_color_night = 5;\n  //\n  string bg_color_night = 6;\n  //\n  string border_color_night = 7;\n  // 1:填充 2:描边 3:填充 + 描边 4:背景不填充 + 背景不描边\n  int32 bg_style = 8;\n  //\n  int32 selected = 9;\n}\n\n//\nmessage RecDislike {\n  //\n  string title = 1;\n  //\n  string sub_title = 2;\n  //\n  string closed_sub_title = 3;\n  //\n  string paste_text = 4;\n  //\n  string closed_paste_text = 5;\n  //\n  repeated DislikeReasons dislike_reason = 6;\n  //\n  string toast = 7;\n  //\n  string closed_toast = 8;\n}\n\n//\nmessage RecThreePoint {\n  //\n  RecDislike dislike = 1;\n  //\n  RecDislike feedback = 2;\n  //\n  bool watch_later = 3;\n}\n\n//\nmessage RefreshPage {\n  //\n  int32 refreshable = 1;\n  //\n  int32 refresh_icon = 2;\n  //\n  string refresh_text = 3;\n  //\n  float refresh_show = 4;\n}\n\n// 相关推荐项\nmessage Relate {\n  //\n  int64 aid = 1;\n  // 封面url\n  string pic = 2;\n  // 标题\n  string title = 3;\n  // UP主信息\n  bilibili.app.archive.v1.Author author = 4;\n  // 稿件状态数\n  bilibili.app.archive.v1.Stat stat = 5;\n  // 时长\n  int64 duration = 6;\n  // 跳转类型\n  // special:pgc视频 av:稿件视频 cm:广告 game:游戏\n  string goto = 7;\n  // 参数（如av号等）\n  string param = 8;\n  // 跳转uri\n  string uri = 9;\n  //\n  string jump_url = 10;\n  // 评分\n  double rating = 11;\n  //\n  string reserve = 12;\n  // 来源标识\n  // operation:管理员添加\n  string from = 13;\n  // 备注\n  string desc = 14;\n  //\n  string rcmd_reason = 15;\n  // 标志文字\n  string badge = 16;\n  // 1P cid\n  int64 cid = 17;\n  //\n  int32 season_type = 18;\n  //\n  int32 rating_count = 19;\n  // 标签文案\n  string tag_name = 20;\n  // 游戏礼包信息\n  PackInfo pack_info = 21;\n  //\n  Notice notice = 22;\n  // 按钮信息\n  Button button = 23;\n  // spm追踪id\n  string trackid = 24;\n  // 游戏卡片新样式\n  int32 new_card = 25;\n  // 推荐理由样式\n  ReasonStyle rcmd_reason_style = 26;\n  //\n  string cover_gif = 27;\n  // 广告\n  CM cm = 28;\n  // 游戏卡字段\n  // 0:下载 1:预约(跳过详情) 2:预约 3:测试 4:测试+预约 5:跳过详情页\n  int64 reserve_status = 29;\n  //\n  string rcmd_reason_extra = 30;\n  //\n  RecThreePoint rec_three_point = 31;\n  //\n  string unique_id = 32;\n  //\n  int64 material_id = 33;\n  //\n  int64 from_source_type = 34;\n  //\n  string from_source_id = 35;\n  //\n  bilibili.app.archive.v1.Dimension dimension = 36;\n  //\n  string cover = 37;\n  //\n  ReasonStyle badge_style = 38;\n  //\n  PowerIconStyle power_icon_style = 39;\n  //\n  string reserve_status_text = 40;\n  //\n  string dislike_report_data = 41;\n  //\n  RankInfo rank_info_game = 42;\n  //\n  string first_frame = 43;\n}\n\n// 相关推荐内容\nmessage RelateItem {\n  // 跳链\n  string url = 1;\n  // 封面\n  string cover = 2;\n}\n\n// 播放页推荐IFS-响应\nmessage RelatesFeedReply {\n  //\n  repeated Relate list = 1;\n  //\n  bool has_next = 2;\n  //\n  bilibili.pagination.PaginationReply pagination = 3;\n}\n\n// 播放页推荐IFS-请求\nmessage RelatesFeedReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n  //\n  string from = 3;\n  //\n  string spmid = 4;\n  //\n  string from_spmid = 5;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 6;\n  //\n  int64 relates_page = 7;\n  //\n  string session_id = 8;\n  //\n  int32 autoplay = 9;\n  //\n  string from_track_id = 10;\n  //\n  string biz_extra = 11;\n  //\n  int64 device_type = 12;\n  //\n  string ad_extra = 13;\n  //\n  bilibili.pagination.Pagination pagination = 14;\n  //\n  int32 refresh_num = 15;\n}\n\n//\nmessage RelateTab {\n  //\n  string id = 1;\n  //\n  string title = 2;\n}\n\n//\nmessage ReplyStyle {\n  //\n  string badge_url = 1;\n  //\n  string badge_text = 2;\n  //\n  int64 badge_type = 3;\n}\n\n// 用户操作状态\nmessage ReqUser {\n  // 用户是否关注UP\n  int32 attention = 1;\n  // UP是否关注用户\n  int32 guest_attention = 2;\n  // 是否收藏\n  int32 favorite = 3;\n  // 是否点赞\n  int32 like = 4;\n  // 是否点踩\n  int32 dislike = 5;\n  // 是否投币\n  int32 coin = 6;\n  // 关注等级\n  int32 attention_level = 7;\n  // 是否收藏合集\n  int32 fav_season = 8;\n  //\n  Button elec_plus_btn = 9;\n}\n\n//\nmessage ReserveReply {\n  //\n  int64 reserve_id = 1;\n}\n\n//\nmessage ReserveReq {\n  //\n  int64 reserve_id = 1;\n  //\n  int64 reserve_action = 2;\n  //\n  int64 up_id = 3;\n}\n\n//\nmessage Restriction {\n  //\n  bool is_teenagers = 1;\n  //\n  bool is_lessons = 2;\n  //\n  bool is_review = 3;\n  //\n  bool disable_rcmd = 4;\n}\n\n// 剧集信息\nmessage Season {\n  //\n  string allow_download = 1;\n  // 剧集ssid\n  int64 season_id = 2;\n  // 是否重定向跳转\n  int32 is_jump = 3;\n  // 剧集标题\n  string title = 4;\n  // 剧集封面url\n  string cover = 5;\n  // 剧集是否完结\n  int32 is_finish = 6;\n  // 最新一话epid\n  int64 newest_ep_id = 7;\n  // 最新一话标题\n  string newest_ep_index = 8;\n  // 总集数\n  int64 total_count = 9;\n  // 更新星期日\n  int32 weekday = 10;\n  // 用户追番标志\n  UserSeason user_season = 11;\n  //\n  SeasonPlayer player = 12;\n  // 单集页面url\n  string ogv_playurl = 13;\n}\n\n//\nmessage SeasonActivityRecordReply {\n  //\n  UgcSeasonActivity activity = 1;\n}\n\n//\nmessage SeasonActivityRecordReq {\n  //\n  int64 season_id = 1;\n  //\n  int64 activity_id = 2;\n  //\n  int32 action = 3;\n  //\n  int64 aid = 4;\n  //\n  int64 cid = 5;\n  //\n  int64 scene = 6;\n  //\n  string spmid = 7;\n}\n\n//\nmessage SeasonPlayer {\n  //\n  int64 aid = 1;\n  //\n  string vid = 2;\n  //\n  int64 cid = 3;\n  //\n  string from = 4;\n}\n\n// 合集详情页-响应\nmessage SeasonReply {\n  // 合集信息\n  UgcSeason season = 1;\n}\n\n// 合集详情页-请求\nmessage SeasonReq {\n  // 合集id\n  int64 season_id = 1;\n}\n\n//\nmessage SeasonShow {\n  //\n  string button_text = 1;\n  //\n  string join_text = 2;\n  //\n  string rule_text = 3;\n  //\n  string checkin_text = 4;\n  //\n  string checkin_prompt = 5;\n}\n\n//\nenum SeasonType {\n  Unknown = 0; //\n  Base = 1; //\n  Good = 2; //\n}\n\n//\nmessage SeasonWidgetExposeReply {\n  //\n  int64 season_id = 1;\n  //\n  int64 activity_id = 2;\n}\n\n//\nmessage SeasonWidgetExposeReq {\n  //\n  int64 mid = 1;\n  //\n  int32 type = 2;\n  //\n  int64 season_id = 3;\n  //\n  int64 activity_id = 4;\n  //\n  int64 aid = 5;\n  //\n  int64 cid = 6;\n  //\n  int64 scene = 7;\n}\n\n// 视频合集小节信息\nmessage Section {\n  // 小节id\n  int64 id = 1;\n  // 小节标题\n  string title = 2;\n  // 小节类型\n  // 0:其他 1:正片\n  int64 type = 3;\n  // 单话列表\n  repeated Episode episodes = 4;\n}\n\n// 短视频下载-响应\nmessage ShortFormVideoDownloadReply {\n  // 是否有下载分享按钮\n  bool has_download_url = 1;\n  // 下载url\n  string download_url = 2;\n  // 文件md5\n  string md5 = 3;\n  // 文件大小(单位为Byte)\n  int64 size = 4;\n  //\n  string backup_download_url = 5;\n}\n\n// 短视频下载-请求\nmessage ShortFormVideoDownloadReq {\n  // 稿件avid\n  int64 aid = 1;\n  // 视频cid\n  int64 cid = 2;\n  // 用户mid\n  int64 mid = 3;\n  // 设备buvid\n  string buvid = 4;\n  // 移动端包类型\n  string mobi_app = 5;\n  // 移动端版本号\n  int64 build = 6;\n  // 运行设备\n  string device = 7;\n  // 平台\n  string platform = 8;\n  // 当前页面spm\n  string spmid = 9;\n  //\n  Restriction restriction = 10;\n  //\n  string tf_isp = 11;\n}\n\n//\nmessage SpecialCell {\n  //\n  string icon = 1;\n  //\n  string icon_night = 2;\n  //\n  string text = 3;\n  //\n  string text_color = 4;\n  //\n  string text_color_night = 5;\n  //\n  string jump_url = 6;\n  //\n  string cell_type = 7;\n  //\n  string cell_bgcolor = 8;\n  //\n  string cell_bgcolor_night = 9;\n  //\n  string param = 10;\n  //\n  string page_title = 11;\n  //\n  string jump_type = 12;\n  //\n  string end_icon = 13;\n  //\n  string end_icon_night = 14;\n  //\n  int64 notes_count = 15;\n}\n\n// 合作成员信息\nmessage Staff {\n  // 成员mid\n  int64 mid = 1;\n  // 成员角色\n  string title = 2;\n  // 成员头像url\n  string face = 3;\n  // 成员昵称\n  string name = 4;\n  // 成员官方信息\n  OfficialVerify official_verify = 5;\n  // 成员会员信息\n  Vip vip = 6;\n  // 是否关注该成员\n  int32 attention = 7;\n  //\n  int32 label_style = 8;\n}\n\n// 标准卡\nmessage StandardCard {\n  // 卡片文案\n  string title = 1;\n  // 按钮文字 未操作\n  string button_title = 2;\n  // 按钮文字 已操作\n  string button_selected_title = 3;\n  // 已操作态是否显示\n  bool show_selected = 4;\n}\n\n// 免流子面板定制化配置\nmessage subTFPanel {\n  // 右侧按钮素材\n  string right_btn_img = 1;\n  // 右侧按钮文案\n  string right_btn_text = 2;\n  // 右侧按钮字体颜色\n  string right_btn_text_color = 3;\n  // 右侧按钮跳转链接\n  string right_btn_link = 4;\n  // 中心主文案内容\n  string main_label = 5;\n  // 运营商\n  string operator = 6;\n}\n\n// TAB\nmessage Tab {\n  // 背景图片\n  string background = 1;\n  // 跳转类型\n  TabOtype otype = 2;\n  // 类型id\n  int64 oid = 3;\n  // 跳转url\n  string uri = 4;\n  // 样式\n  TabStyle style = 5;\n  // 文字\n  string text = 6;\n  // 未选中态字色\n  string text_color = 7;\n  // 选中态字色\n  string text_color_selected = 8;\n  // 图片\n  string pic = 9;\n  // 后台配置自增\n  int64 id = 10;\n  //\n  google.protobuf.Any ad_tab_info = 11;\n}\n\n// TAB跳转类型\nenum TabOtype {\n  UnknownOtype = 0; // 未知类型\n  URL = 1; // url链接\n  TopicNA = 2; // native话题活动\n  CmURI = 3; // 广告url\n}\n\n// TAB样式\nenum TabStyle {\n  UnknownStyle = 0; // 未知样式\n  Text = 1; // 文字样式\n  Pic = 2; // 图片样式\n}\n\n// TAG信息\nmessage Tag {\n  // TAD id\n  int64 id = 1;\n  // TAG名\n  string name = 2;\n  //\n  int64 likes = 3;\n  //\n  int64 hates = 4;\n  //\n  int32 liked = 5;\n  //\n  int32 hated = 6;\n  // TAG页面uri\n  string uri = 7;\n  // TAG类型\n  // common:普通 new:话题 act:活动\n  string tag_type = 8;\n}\n\n// 免流面板定制\nmessage TFPanelCustomized {\n  // 右侧按钮素材\n  string right_btn_img = 1;\n  // 右侧按钮文案\n  string right_btn_text = 2;\n  // 右侧按钮字体颜色\n  string right_btn_text_color = 3;\n  // 右侧按钮跳转链接\n  string right_btn_link = 4;\n  // 中心主文案内容\n  string main_label = 5;\n  // 运营商(cm ct cu)\n  string operator = 6;\n  // 子面板定制化配置\n  map<string, subTFPanel> sub_panel = 7;\n}\n\n// TAG图标信息\nmessage TIcon {\n  // TAG图标url\n  string icon = 1;\n}\n\n// UGC视频合集信息\nmessage UgcSeason {\n  // 合集id\n  int64 id = 1;\n  // 合集标题\n  string title = 2;\n  // 合集封面url\n  string cover = 3;\n  // 合集简介\n  string intro = 4;\n  // 小节列表\n  repeated Section sections = 5;\n  // 合集状态数\n  UgcSeasonStat stat = 6;\n  // 标签字色\n  string label_text = 7;\n  // 标签背景色\n  string label_text_color = 8;\n  // 标签夜间字色\n  string label_bg_color = 9;\n  // 标签夜间背景色\n  string label_text_night_color = 10;\n  // 右侧描述文案\n  string label_bg_night_color = 11;\n  // 按钮文案\n  string descRight = 12;\n  // 分集总数\n  int64 ep_count = 13;\n  // 合集类型\n  SeasonType season_type = 14;\n  //\n  bool show_continual_button = 15;\n  //\n  int64 ep_num = 16;\n  //\n  bool season_pay = 17;\n  //\n  GoodsInfo goods_info = 18;\n  //\n  ButtonStyle pay_button = 19;\n  //\n  string label_text_new = 20;\n  //\n  UgcSeasonActivity activity = 21;\n  //\n  repeated string season_ability = 22;\n}\n\n//\nmessage UgcSeasonActivity {\n  //\n  int32 type = 1;\n  //\n  int64 oid = 2;\n  //\n  int64 activity_id = 3;\n  //\n  string title = 4;\n  //\n  string intro = 5;\n  //\n  int32 day_count = 6;\n  //\n  int32 user_count = 7;\n  //\n  int64 join_deadline = 8;\n  //\n  int64 activity_deadline = 9;\n  //\n  int32 checkin_view_time = 10;\n  //\n  bool new_activity = 11;\n  //\n  UserActivity user_activity = 12;\n  //\n  SeasonShow season_show = 13;\n}\n\n// ugc视频合集状态数\nmessage UgcSeasonStat {\n  // 合集id\n  int64 season_id = 1;\n  // 观看数\n  int64 view = 2;\n  // 弹幕数\n  int32 danmaku = 3;\n  // 评论数\n  int32 reply = 4;\n  // 收藏数\n  int32 fav = 5;\n  // 投币数\n  int32 coin = 6;\n  // 分享数\n  int32 share = 7;\n  // 当前排名\n  int32 now_rank = 8;\n  // 历史最高排名\n  int32 his_rank = 9;\n  // 总计点赞\n  int32 like = 10;\n}\n\n//\nmessage UpAct {\n  //\n  int64 sid = 1;\n  //\n  int64 mid = 2;\n  //\n  string title = 3;\n  //\n  string statement = 4;\n  //\n  string image = 5;\n  //\n  string url = 6;\n  //\n  string button = 7;\n}\n\n//\nmessage UpLikeImg {\n  //\n  string pre_img = 1;\n  //\n  string suc_img = 2;\n  //\n  string content = 3;\n  //\n  int64 type = 4;\n}\n\n// UP主信息\nmessage UpperInfos {\n  // 粉丝数\n  int64 fans_count = 1;\n  // 近半年投稿数\n  int64 arc_count_last_half_year = 2;\n  // 成为UP主时间\n  int64 first_up_dates = 3;\n  // 总播放量\n  int64 total_play_count = 4;\n}\n\n//\nmessage UserActivity {\n  //\n  int32 user_state = 1;\n  //\n  int64 last_checkin_date = 2;\n  //\n  int32 checkin_today = 3;\n  //\n  int32 user_day_count = 4;\n  //\n  int32 user_view_time = 5;\n  //\n  string portrait = 6;\n}\n\n// 用户装扮信息\nmessage UserGarb {\n  // 点赞动画url\n  string url_image_ani_cut = 1;\n  //\n  string like_toast = 2;\n}\n\n// 用户追番标志\nmessage UserSeason {\n  // 关注状态\n  // 0:未关注 1:已关注\n  string attention = 1;\n}\n\n// 视频引导信息\nmessage VideoGuide {\n  // 关注按钮卡片\n  repeated Attention attention = 1;\n  // 互动弹幕\n  repeated CommandDm commandDms = 2;\n  // 运营卡片\n  repeated OperationCard operation_card = 3;\n  // 运营卡片新版\n  repeated OperationCardNew operation_card_new = 4;\n  // 契约卡\n  ContractCard contract_card = 5;\n  //\n  repeated OperationCardV2 cards_second = 6;\n}\n\n//\nmessage VideoPoint {\n  //\n  int32 type = 1;\n  //\n  int64 from = 2;\n  //\n  int64 to = 3;\n  //\n  string content = 4;\n  //\n  string cover = 5;\n  //\n  string logo_url = 6;\n}\n\n//\nmessage VideoShot {\n  //\n  string pv_data = 1;\n  //\n  int32 img_x_len = 2;\n  //\n  int32 img_y_len = 3;\n  //\n  int32 img_x_size = 4;\n  //\n  int32 img_y_size = 5;\n  //\n  repeated string image = 6;\n}\n\n//\nmessage ViewMaterial {\n  //\n  int64 oid = 1;\n  //\n  int64 mid = 2;\n  //\n  string title = 3;\n  //\n  string author = 4;\n  //\n  string jump_url = 5;\n}\n\n//\nmessage ViewMaterialReply {\n  //\n  repeated MaterialRes material_res = 1;\n  //\n  MaterialLeft material_left = 2;\n}\n\n//\nmessage ViewMaterialReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n  //\n  int64 cid = 3;\n}\n\n// 分P信息\nmessage ViewPage {\n  // 分P基本信息\n  bilibili.app.archive.v1.Page page = 1;\n  // 分P对应的音频稿件\n  Audio audio = 2;\n  // 分P弹幕信息\n  DM dm = 3;\n  // 下载文案\n  string download_title = 4;\n  // 分P完整标题(视频标题+分P标题)\n  string download_subtitle = 5;\n}\n\n// 稿件播放中数据-回复\nmessage ViewProgressReply {\n  // 视频引导信息\n  VideoGuide video_guide = 1;\n  // Chronos灰度管理\n  Chronos chronos = 2;\n  // 视频快照\n  VideoShot arc_shot = 3;\n  //\n  repeated VideoPoint points = 4;\n  //\n  PointMaterial point_material = 5;\n  //\n  bool point_permanent = 6;\n  // 名词解释列表\n  repeated BuzzwordConfig buzzword_periods = 7;\n}\n\n// 稿件播放中数据-请求\nmessage ViewProgressReq {\n  // 稿件avid\n  int64 aid = 1;\n  // 视频cid\n  int64 cid = 2;\n  // UP主mid\n  int64 up_mid = 3;\n  //\n  string engine_version = 4;\n  //\n  string message_protocol = 5;\n  //\n  string service_key = 6;\n}\n\n// 视频页信息-响应\nmessage ViewReply {\n  // 稿件信息\n  bilibili.app.archive.v1.Arc arc = 1;\n  // 分P信息\n  repeated ViewPage pages = 2;\n  // UP主扩展信息 (\"OnwerExt\"为源码中拼写错误)\n  OnwerExt owner_ext = 3;\n  // 稿件用户操作状态\n  ReqUser req_user = 4;\n  // 稿件TAG\n  repeated Tag tag = 5;\n  // TAG对应的图标\n  map<string, TIcon> t_icon = 6;\n  // 稿件映射的PGC剧集信息\n  Season season = 7;\n  // 充电排行\n  ElecRank elec_rank = 8;\n  // 历史观看进度\n  History history = 9;\n  // 视频相关推荐列表\n  repeated Relate relates = 10;\n  // 不感兴趣原因\n  Dislike dislike = 11;\n  // 播放图标动画配置档\n  PlayerIcon player_icon = 12;\n  //\n  string vip_active = 13;\n  // 稿件bvid\n  string bvid = 14;\n  // 获得荣誉信息\n  Honor honor = 15;\n  // 相关推荐顶部tab\n  repeated RelateTab relate_tab = 16;\n  // 参与的活动页面url\n  string activity_url = 17;\n  // 稿件引用bgm列表\n  repeated Bgm bgm = 18;\n  // 联合投稿成员列表\n  repeated Staff staff = 19;\n  // 争议信息\n  string argue_msg = 20;\n  // 短链接\n  string short_link = 21;\n  // 播放实验\n  // 1:相关推荐自动播放\n  int32 play_param = 22;\n  // 标签\n  Label label = 23;\n  // UGC视频合集信息\n  UgcSeason ugc_season = 24;\n  // 配置信息\n  Config config = 25;\n  // 分享副标题(已观看xxx次)\n  string share_subtitle = 26;\n  // 互动视频信息\n  Interaction interaction = 27;\n  // 错误码\n  // DEFAULT:正常 CODE404:视频被UP主删除\n  ECode ecode = 28;\n  // 404页信息\n  CustomConfig custom_config = 29;\n  // 广告\n  repeated CM cms = 30;\n  // 广告配置\n  CMConfig cm_config = 31;\n  // 播放页定制tab\n  Tab tab = 32;\n  // 排行榜\n  Rank rank = 33;\n  // 免流面板定制\n  TFPanelCustomized tf_panel_customized = 34;\n  // UP主发起活动\n  UpAct up_act = 35;\n  // 用户装扮\n  UserGarb user_garb = 36;\n  // 大型活动合集\n  ActivitySeason activity_season = 37;\n  // 评论样式\n  string badge_url = 38;\n  // 直播预约信息\n  LiveOrderInfo live_order_info = 39;\n  // 稿件简介v2\n  repeated DescV2 desc_v2 = 40;\n  //\n  CmIpad cm_ipad = 41;\n  //\n  repeated ViewMaterial sticker = 42;\n  //\n  UpLikeImg up_like_img = 43;\n  //\n  LikeCustom like_custom = 44;\n  //\n  repeated Tag desc_tag = 45;\n  //\n  SpecialCell special_cell = 46;\n  //\n  Online online = 47;\n  //\n  google.protobuf.Any cm_under_player = 48;\n  //\n  repeated ViewMaterial video_source = 49;\n  //\n  repeated SpecialCell special_cell_new = 50;\n  //\n  PremiereResource premiere = 51;\n  //\n  bool refresh_special_cell = 52;\n  //\n  MaterialLeft material_left = 53;\n  //\n  int64 notes_count = 54;\n  //\n  PullClientAction pull_action = 55;\n  //\n  ArcExtra arc_extra = 56;\n  //\n  bilibili.pagination.PaginationReply pagination = 57;\n  //\n  LikeAnimation like_animation = 58;\n  //\n  ReplyStyle reply_preface = 59;\n  //\n  RefreshPage refresh_page = 60;\n  //\n  CoinCustom coin_custom = 61;\n}\n\n// 视频页详情页-请求\nmessage ViewReq {\n  // 稿件avid(av/bv任选其一)\n  int64 aid = 1;\n  // 稿件bvid(av/bv任选其一)\n  string bvid = 2;\n  // 来源\n  string from = 3;\n  // AI trackid\n  string trackid = 4;\n  // 广告扩展数据\n  string ad_extra = 5;\n  // 清晰度(旧版)\n  int32 qn = 6;\n  // 流版本(旧版)\n  int32 fnver = 7;\n  // 流类型(旧版)\n  int32 fnval = 8;\n  // 是否强制使用域名(旧版)\n  int32 force_host = 9;\n  // 是否允许4K(旧版)\n  int32 fourk = 10;\n  // 当前页面spm\n  string spmid = 11;\n  // 上一页面spm\n  string from_spmid = 12;\n  //\n  int32 autoplay = 13;\n  // 视频秒开参数\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 14;\n  //\n  string page_version = 15;\n  //\n  string biz_extra = 16;\n  //\n  int64 device_type = 17;\n  //\n  int64 relates_page = 18;\n  //\n  string session_id = 19;\n  //\n  int32 in_feed_play = 20;\n  //\n  string play_mode = 21;\n  //\n  bilibili.pagination.Pagination pagination = 22;\n  //\n  int32 refresh = 23;\n  //\n  int32 refresh_num = 24;\n}\n\n//\nmessage ViewTagReply {\n  //\n  repeated SpecialCell special_cell_new = 1;\n  //\n  MaterialLeft material_left = 2;\n}\n\n//\nmessage ViewTagReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n  //\n  int64 cid = 3;\n  //\n  string spmid = 4;\n}\n\n// 会员信息\nmessage Vip {\n  //会员类型\n  int32 type = 1;\n  //到期时间\n  int64 due_date = 2;\n  //\n  string due_remark = 3;\n  //\n  int32 access_status = 4;\n  //会员状态\n  int32 vip_status = 5;\n  //\n  string vip_status_warn = 6;\n  //\n  int32 theme_type = 7;\n  //\n  VipLabel label = 8;\n}\n\n// 会员类型标签\nmessage VipLabel {\n  //\n  string path = 1;\n  //\n  string text = 2;\n  //\n  string label_theme = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/viewunite/common.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.viewunite.common;\n\noption java_multiple_files = true;\n\nimport \"bilibili/dagw/component/avatar/v1/avatar.proto\";\nimport \"bilibili/pagination/pagination.proto\";\nimport \"google/protobuf/any.proto\";\n\n//\nmessage Activity {\n  //\n  int32 id = 1;\n  //\n  string title = 2;\n  //\n  string link = 3;\n  //\n  string cover = 4;\n  //\n  int32 type = 5;\n  //\n  string ab = 6;\n  //\n  string show_name = 7;\n  //\n  string picurl = 8;\n  //\n  string picurl_selected = 9;\n  //\n  string h5_link = 10;\n  //\n  string jump_mode = 11;\n  //\n  repeated Item items = 12;\n}\n\n//\nmessage ActivityEntrance {\n  //\n  string activity_cover = 1;\n  //\n  string activity_title = 2;\n  //\n  string word_tag = 3;\n  //\n  string activity_subtitle = 4;\n  //\n  string activity_link = 5;\n  //\n  int32 activity_type = 6;\n  //\n  int32 reserve_id = 7;\n  //\n  int32 status = 8;\n  //\n  repeated User upper_list = 9;\n  //\n  map<string, string> report = 10;\n}\n\n//\nmessage ActivityEntranceModule {\n  //\n  repeated ActivityEntrance activity_entrance = 1;\n}\n\n//\nmessage ActivityReserve {\n  //\n  string title = 1;\n  //\n  StatInfo vt = 2;\n  //\n  StatInfo danmaku = 3;\n  //\n  ReserveButton button = 4;\n}\n\n//\nmessage ActivityResource {\n  //\n  string mod_pool_name = 1;\n  //\n  string mod_resource_name = 2;\n}\n\n//\nmessage ActivityTab {\n  //\n  int32 id = 1;\n  //\n  string title = 2;\n  //\n  int32 type = 3;\n  //\n  string show_name = 4;\n  //\n  string picurl = 5;\n  //\n  string picurl_selected = 6;\n  //\n  string h5_link = 7;\n  //\n  string link = 8;\n  //\n  int32 link_type = 9;\n  //\n  int64 biz_key = 10;\n  //\n  string desc = 11;\n  //\n  string act_ext = 12;\n  //\n  map<string, string> report = 13;\n}\n\n//\nmessage AggEpCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string icon = 3;\n  //\n  int32 num = 4;\n  //\n  string jump_url = 5;\n}\n\n//\nmessage AggEps {\n  //\n  repeated AggEpCard agg_ep_cards = 1;\n  //\n  int32 place_index = 2;\n}\n\n//\nmessage AttentionRecommend {}\n\n//\nenum AttentionRelationStatus {\n  //\n  ARS_NONE = 0;\n  //\n  ARS_N0RELATION = 1;\n  //\n  ARS_FOLLOWHIM = 2;\n  //\n  ARS_FOLLOWME = 3;\n  //\n  ARS_BUDDY = 4;\n  //\n  ARS_SPECIAL = 5;\n  //\n  ARS_CANCELBLOCK = 6;\n}\n\n//\nmessage Audio {\n  //\n  map<int64, AudioInfo> audio_info = 1;\n}\n\n//\nmessage AudioInfo {\n  //\n  string title = 1;\n  //\n  string cover_url = 2;\n  //\n  int64 song_id = 3;\n  //\n  int64 play_count = 4;\n  //\n  int64 reply_count = 5;\n  //\n  int64 upper_id = 6;\n  //\n  string entrance = 7;\n  //\n  int64 song_attr = 8;\n}\n\n//\nmessage BadgeInfo {\n  //\n  string text = 1;\n  //\n  string text_color = 2;\n  //\n  string text_color_night = 3;\n  //\n  string bg_color = 4;\n  //\n  string bg_color_night = 5;\n  //\n  string border_color = 6;\n  //\n  string border_color_night = 7;\n  //\n  int32 bg_style = 8;\n  //\n  string img = 9;\n  //\n  int32 type = 10;\n}\n\n//\nmessage Banner {\n  //\n  string title = 1;\n  //\n  repeated RelateItem relate_item = 2;\n}\n\n//\nmessage BizFavParam {\n  //\n  int64 season_id = 1;\n}\n\n//\nmessage BizReserveActivityParam {\n  //\n  int64 activity_id = 1;\n  //\n  string from = 2;\n  //\n  string type = 3;\n  //\n  int64 oid = 4;\n  //\n  int64 reserve_id = 5;\n}\n\n//\nmessage Button {\n  //\n  string title = 1;\n  //\n  string left_strikethrough_text = 2;\n  //\n  string type = 3;\n  //\n  string link = 4;\n  //\n  BadgeInfo badge_info = 5;\n  //\n  string sub_title = 6;\n}\n\n//\nmessage CardBasicInfo {\n  //\n  string title = 1;\n  //\n  string desc = 2;\n  //\n  string cover = 3;\n  //\n  string uri = 4;\n  //\n  string track_id = 5;\n  //\n  string unique_id = 6;\n  //\n  int64 from_source_type = 7;\n  //\n  string from_source_id = 8;\n  //\n  int64 material_id = 9;\n  //\n  string cover_gif = 10;\n  //\n  Owner author = 11;\n  //\n  int64 id = 12;\n  //\n  string from = 13;\n  //\n  string from_spmid_suffix = 14;\n  //\n  string report_flow_data = 15;\n}\n\n//\nmessage CardStyle {\n  //\n  int32 id = 1;\n  //\n  string name = 2;\n}\n\n//\nmessage Celebrity {\n  //\n  int32 id = 1;\n  //\n  string name = 2;\n  //\n  string role = 3;\n  //\n  string avatar = 4;\n  //\n  string short_desc = 5;\n  //\n  string desc = 6;\n  //\n  string character_avatar = 7;\n  //\n  string link = 8;\n  //\n  int64 mid = 9;\n  //\n  int32 is_follow = 10;\n  //\n  string occupation_name = 11;\n  //\n  int32 occupation_type = 12;\n  //\n  int32 relate_attr = 13;\n  //\n  string small_avatar = 14;\n  //\n  map<string, string> report = 15;\n}\n\n//\nmessage CharacterGroup {\n  //\n  string title = 1;\n  //\n  repeated Celebrity characters = 2;\n}\n\n//\nmessage Characters {\n  //\n  repeated CharacterGroup groups = 1;\n}\n\n//\nmessage CoinExtend {\n  //\n  string coin_app_zip_icon = 1;\n  //\n  string coin_app_icon_1 = 2;\n  //\n  string coin_app_icon_2 = 3;\n  //\n  string coin_app_icon_3 = 4;\n  //\n  string coin_app_icon_4 = 5;\n}\n\n//\nmessage CombinationEp {\n  //\n  int32 id = 1;\n  //\n  int32 section_id = 2;\n  //\n  string title = 3;\n  //\n  int32 can_ord_desc = 4;\n  //\n  string more = 5;\n  //\n  repeated int32 episode_ids = 6;\n  //\n  repeated ViewEpisode episodes = 7;\n  //\n  string split_text = 8;\n  //\n  Style module_style = 9;\n  //\n  repeated SerialSeason serial_season = 10;\n  //\n  SectionData section_data = 11;\n}\n\n//\nmessage Covenanter {}\n\n//\nmessage DeliveryData {\n  //\n  string title = 1;\n  //\n  Style module_style = 2;\n  //\n  string more = 3;\n  //\n  oneof data {\n    //\n    Activity activity = 4;\n    //\n    Characters characters = 5;\n    //\n    TheatreHotTopic theatre_hot_topic = 6;\n    //\n    AggEps agg_eps = 7;\n  }\n  //\n  int32 id = 8;\n  //\n  map<string, string> report = 9;\n}\n\n//\nmessage Desc {\n  //\n  string info = 1;\n  //\n  string title = 2;\n}\n\nenum DescType {\n  //\n  DescTypeUnknown = 0;\n  //\n  DescTypeText = 1;\n  //\n  DescTypeAt = 2;\n}\n\n//\nmessage DescV2 {\n  //\n  string text = 1;\n  //\n  int32 type = 2;\n  //\n  string uri = 3;\n  //\n  int64 rid = 4;\n}\n\n//\nmessage Dimension {\n  //\n  int64 width = 1;\n  //\n  int64 height = 2;\n  //\n  int64 rotate = 3;\n}\n\n//\nmessage DislikeReasons {\n  //\n  int64 id = 1;\n  //\n  int64 mid = 2;\n  //\n  int32 rid = 3;\n  //\n  int64 tag_id = 4;\n  //\n  string name = 5;\n}\n\n//\nmessage FollowLayer {\n  //\n  Staff staff = 1;\n  //\n  Desc desc = 2;\n  //\n  map<string, string> report = 3;\n}\n\n//\nmessage Headline {\n  //\n  Label label = 1;\n  //\n  string content = 2;\n}\n\n//\nmessage HistoryNode {\n  //\n  int64 node_id = 1;\n  //\n  string title = 2;\n  //\n  int64 cid = 3;\n}\n\n// 荣誉 Banner\nmessage Honor {\n  //\n  string icon = 1;\n  //\n  string icon_night = 2;\n  //\n  string text = 3;\n  //\n  string text_extra = 4;\n  //\n  string text_color = 5;\n  //\n  string text_color_night = 6;\n  //\n  string bg_color = 7;\n  //\n  string bg_color_night = 8;\n  //\n  string url = 9;\n  //\n  string url_text = 10;\n  //\n  HonorType type = 11;\n  //\n  HonorJumpType honor_jump_type = 12;\n  //\n  map<string, string> report = 13;\n}\n\n// 荣誉 Banner 跳转类型\nenum HonorJumpType {\n  //\n  HONOR_JUMP_TYPE_UNKNOWN = 0;\n  //\n  HONOR_OPEN_URL = 1;\n  //\n  HONOR_HALF_SCREEN = 2;\n}\n\n// 荣誉类型\nenum HonorType {\n  //\n  HONOR_NONE = 0;\n  //\n  PLAYLET = 1;\n  // 视频存在争议\n  ARGUE = 2;\n  //\n  NOTICE = 3;\n  //\n  GUIDANCE = 4;\n  // 哔哩哔哩榜\n  HONOR_BILI_RANK = 5;\n  // 周榜\n  HONOR_WEEKLY_RANK = 6;\n  // 日榜\n  HONOR_DAILY_RANK = 7;\n  //\n  HONOR_CHANNEL = 8;\n  // 音乐榜?\n  HONOR_MUSIC = 9;\n  //\n  HONOR_REPLY = 10;\n}\n\n//\nmessage IconFont {\n  //\n  string name = 1;\n  //\n  string text = 2;\n}\n\n//\nmessage Interaction {\n  //\n  int64 ep_id = 1;\n  //\n  HistoryNode history_node = 2;\n  //\n  int64 graph_version = 3;\n  //\n  string msg = 4;\n  //\n  bool is_interaction = 5;\n}\n\n//\nmessage Item {\n  //\n  string link = 1;\n  //\n  string cover = 2;\n}\n\n//\nmessage KingPos {\n  //\n  bool disable = 1;\n  //\n  string icon = 2;\n  //\n  KingPositionType type = 3;\n  //\n  string disable_toast = 4;\n  //\n  string checked_post = 5;\n  //\n  oneof extend {\n    //\n    LikeExtend like = 6;\n    //\n    CoinExtend coin = 7;\n  }\n}\n\n//\nmessage KingPosition {\n  //\n  repeated KingPos king_pos = 1;\n  //\n  repeated KingPos extenf = 2;\n}\n\n//\nenum KingPositionType {\n  //\n  KING_POS_UNSPECIFIED = 0;\n  //\n  LIKE = 1;\n  //\n  DISLIKE = 2;\n  //\n  COIN = 3;\n  //\n  FAV = 4;\n  //\n  SHARE = 5;\n  //\n  CACHE = 6;\n  //\n  DANMAKU = 7;\n}\n\n//\nmessage Label {\n  //\n  int32 type = 1;\n  //\n  string uri = 2;\n  //\n  string icon = 3;\n  //\n  string icon_night = 4;\n  //\n  int64 icon_width = 5;\n  //\n  int64 icon_height = 6;\n  //\n  string lottie = 7;\n  //\n  string lottie_night = 8;\n}\n\n//\nmessage LikeComment {\n  //\n  string reply = 1;\n  //\n  string title = 2;\n}\n\n//\nmessage LikeExtend {\n  //\n  UpLikeImg triple_like = 1;\n  //\n  string like_animation = 2;\n  //\n  PlayerAnimation player_animation = 3;\n  //\n  ActivityResource resource = 4;\n}\n\n//\nmessage Live {\n  //\n  int64 mid = 1;\n  //\n  int64 room_id = 2;\n  //\n  string uri = 3;\n  //\n  string endpage_uri = 4;\n}\n\n// 直播预约信息\nmessage LiveOrder {\n  //\n  int64 sid = 1;\n  //\n  string text = 2;\n  //\n  int64 live_plan_start_time = 3;\n  //\n  bool is_follow = 4;\n  //\n  int64 follow_count = 5;\n}\n\n//\nmessage Mine {\n  //\n  double amount = 1;\n  //\n  int32 rank = 2;\n  //\n  string msg = 3;\n}\n\n//\nmessage Module {\n  //\n  ModuleType type = 1;\n  //\n  oneof data {\n    //\n    OgvIntroduction ogv_introduction = 2;\n    //\n    UgcIntroduction ugc_introduction = 3;\n    //\n    KingPosition king_position = 4;\n    //\n    Headline head_line = 5;\n    //\n    OgvTitle ogv_title = 6;\n    //\n    Honor honor = 7;\n    //\n    UserList list = 8;\n    //\n    Staffs staffs = 9;\n    //\n    ActivityReserve activity_reserve = 10;\n    //\n    LiveOrder live_order = 11;\n    //\n    SectionData section_data = 12;\n    //\n    DeliveryData delivery_data = 13;\n    //\n    FollowLayer follow_layer = 14;\n    //\n    OgvSeasons ogv_seasons = 15;\n    //\n    UgcSeasons ugc_season = 16;\n    //\n    OgvLiveReserve ogv_live_reserve = 17;\n    //\n    CombinationEp combination_ep = 18;\n    //\n    Sponsor sponsor = 19;\n    //\n    ActivityEntranceModule activity_entrance_module = 20;\n    //\n    SerialSeason serial_season = 21;\n    //\n    Relates relates = 22;\n    //\n    Banner banner = 23;\n    //\n    Audio audio = 24;\n    //\n    LikeComment like_comment = 25;\n    //\n    AttentionRecommend attention_recommend = 26;\n    //\n    Covenanter covenanter = 27;\n  }\n}\n\nenum ModuleType {\n  //\n  UNKNOWN = 0;\n  //\n  OGV_INTRODUCTION = 1;\n  //\n  OGV_TITLE = 2;\n  //\n  UGC_HEADLINE = 3;\n  //\n  UGC_INTRODUCTION = 4;\n  //\n  KING_POSITION = 5;\n  //\n  MASTER_USER_LIST = 6;\n  //\n  STAFFS = 7;\n  //\n  HONOR = 8;\n  //\n  OWNER = 9;\n  //\n  PAGE = 10;\n  //\n  ACTIVITY_RESERVE = 11;\n  //\n  LIVE_ORDER = 12;\n  //\n  POSITIVE = 13;\n  //\n  SECTION = 14;\n  //\n  RELATE = 15;\n  //\n  PUGV = 16;\n  //\n  COLLECTION_CARD = 17;\n  //\n  ACTIVITY = 18;\n  //\n  CHARACTER = 19;\n  //\n  FOLLOW_LAYER = 20;\n  //\n  OGV_SEASONS = 21;\n  //\n  UGC_SEASON = 22;\n  //\n  OGV_LIVE_RESERVE = 23;\n  //\n  COMBINATION_EPISODE = 24;\n  //\n  SPONSOR = 25;\n  //\n  ACTIVITY_ENTRANCE = 26;\n  //\n  THEATRE_HOT_TOPIC = 27;\n  //\n  RELATED_RECOMMEND = 28;\n  //\n  PAY_BAR = 29;\n  //\n  BANNER = 30;\n  //\n  AUDIO = 31;\n  //\n  AGG_CARD = 32;\n  //\n  SINGLE_EP = 33;\n  //\n  LIKE_COMMENT = 34;\n  //\n  ATTENTION_RECOMMEND = 35;\n  //\n  COVENANTER = 36;\n}\n\n//\nmessage MultiViewEp {\n  //\n  int64 ep_id = 1;\n}\n\n//\nmessage NewEp {\n  //\n  int32 id = 1;\n  //\n  string title = 2;\n  //\n  string desc = 3;\n  //\n  int32 is_new = 4;\n  //\n  string more = 5;\n  //\n  string cover = 6;\n  //\n  string index_show = 7;\n}\n\n//\nenum OccupationType {\n  //\n  STAFF = 0;\n  //\n  CAST = 1;\n}\n\n//\nmessage OfficialVerify {\n  //\n  int32 type = 1;\n  //\n  string desc = 2;\n}\n\n//\nmessage OgvIntroduction {\n  //\n  string followers = 1;\n  //\n  string score = 2;\n  //\n  StatInfo play_data = 3;\n}\n\n//\nmessage OgvLiveReserve {\n  //\n  int64 reserve_id = 1;\n  //\n  string title = 2;\n  //\n  string icon = 3;\n  //\n  string night_icon = 4;\n  //\n  string click_button = 5;\n  //\n  string link = 6;\n  //\n  int32 follow_video_is_reserve_live = 7;\n  //\n  string bg_color = 8;\n  //\n  string night_bg_color = 9;\n  //\n  string text_color = 10;\n  //\n  string night_text_color = 11;\n  //\n  string bt_bg_color = 12;\n  //\n  string bt_frame_color = 13;\n  //\n  string night_bt_bg_color = 14;\n  //\n  string night_bt_frame_color = 15;\n  //\n  int32 active_type = 16;\n  //\n  int32 reserve_status = 17;\n  //\n  string bt_text_color = 18;\n  //\n  string night_bt_text_color = 19;\n  //\n  map<string, string> report = 20;\n}\n\n//\nmessage OgvSeasons {\n  //\n  string title = 1;\n  //\n  repeated SerialSeason serial_season = 2;\n  //\n  SerialSeasonCoverStyle style = 3;\n}\n\n//\nmessage OgvTitle {\n  //\n  string title = 1;\n  //\n  BadgeInfo badge_info = 2;\n  //\n  int32 is_show_btn_animation = 3;\n  //\n  int32 follow_video_is_reserve_live = 4;\n  //\n  int64 reserve_id = 5;\n  //\n  TitleDeliveryButton title_delivery_button = 6;\n}\n\n//\nmessage Owner {\n  bilibili.dagw.component.avatar.v1.AvatarItem avatar = 1;\n  //\n  string url = 2;\n  //\n  string title = 3;\n  //\n  string fans = 4;\n  //\n  string arc_count = 5;\n  //\n  int32 attention = 6;\n  //\n  int32 attention_relation = 7;\n  //\n  string pub_location = 8;\n  //\n  Vip vip = 9;\n  //\n  string title_url = 10;\n  //\n  string face = 11;\n  //\n  int64 mid = 12;\n  //\n  OfficialVerify official_verify = 13;\n  //\n  Live live = 14;\n  //\n  int64 fans_num = 15;\n  //\n  repeated int64 assists = 16;\n}\n\n//\nmessage Page {\n  //\n  int64 cid = 1;\n  //\n  string part = 2;\n  //\n  int64 duration = 3;\n  //\n  string desc = 4;\n  //\n  Dimension dimension = 5;\n  //\n  string dl_title = 6;\n  //\n  string dl_subtitle = 7;\n}\n\n//\nmessage Pendant {\n  //\n  int32 pid = 1;\n  //\n  string name = 2;\n  //\n  string image = 3;\n}\n\n//\nmessage PlayerAnimation {\n  //\n  string player_icon = 1;\n  //\n  string player_triple_icon = 2;\n}\n\n//\nmessage PointActivity {\n  //\n  string tip = 1;\n  //\n  string content = 2;\n  //\n  string link = 3;\n}\n\n//\nmessage PowerIconStyle {\n  //\n  string icon_url = 1;\n  //\n  string icon_night_url = 2;\n  //\n  int64 icon_width = 3;\n  //\n  int64 icon_height = 4;\n}\n\n//\nmessage Rank {\n  //\n  string icon = 1;\n  //\n  string icon_night = 2;\n  //\n  string text = 3;\n}\n\n//\nmessage RankInfo {\n  //\n  string icon_url_night = 1;\n  //\n  string icon_url_day = 2;\n  //\n  string bkg_night_color = 3;\n  //\n  string bkg_day_color = 4;\n  //\n  string font_night_color = 5;\n  //\n  string font_day_color = 6;\n  //\n  string rank_content = 7;\n  //\n  string rank_link = 8;\n}\n\n//\nmessage Rating {\n  //\n  string score = 1;\n  //\n  int32 count = 2;\n}\n\n// 视频详情下方推荐卡子类型: 普通视频\nmessage RelateAVCard {\n  //\n  int64 duration = 1;\n  //\n  int64 cid = 2;\n  //\n  Dimension dimension = 3;\n  //\n  Stat stat = 4;\n  //\n  string jump_url = 5;\n  //\n  bool show_up_name = 6;\n  //\n  BadgeInfo rcmd_reason = 7;\n}\n\n// 视频详情下方推荐卡子类型: 番剧(小卡?)\nmessage RelateBangumiAvCard {\n  //\n  BadgeInfo badge = 1;\n  //\n  Stat stat = 2;\n  //\n  Rating rating = 3;\n}\n\n// 视频详情下方推荐卡子类型: 番剧(大卡?)\nmessage RelateBangumiCard {\n  //\n  int32 season_id = 1;\n  //\n  int32 season_type = 2;\n  //\n  NewEp new_ep = 3;\n  //\n  Stat stat = 4;\n  //\n  Rating rating = 5;\n  //\n  string rcmd_reason = 6;\n  //\n  BadgeInfo badge_info = 7;\n  //\n  string goto_type = 8;\n  //\n  map<string, string> report = 9;\n}\n\n// 视频详情下方推荐卡子类型: 番剧集?\nmessage RelateBangumiResourceCard {\n  //\n  int32 type = 1;\n  //\n  string scover = 2;\n  //\n  int32 re_type = 3;\n  //\n  string re_value = 4;\n  //\n  string corner = 5;\n  //\n  int32 card = 6;\n  //\n  string siz = 7;\n  //\n  int32 position = 8;\n  //\n  string rcmd_reason = 9;\n  //\n  string label = 10;\n  //\n  map<string, string> report = 11;\n  //\n  string goto_type = 12;\n}\n\n// 视频详情下方推荐卡子类型: UGC 番剧?\nmessage RelateBangumiUgcCard {\n  //\n  BadgeInfo badge = 1;\n  //\n  Stat stat = 2;\n  //\n  Rating rating = 3;\n}\n\n// 视频详情下方推荐卡\nmessage RelateCard {\n  //\n  RelateCardType relate_card_type = 1;\n  //\n  oneof card {\n    //\n    RelateAVCard av = 2;\n    //\n    RelateBangumiCard bangumi = 3;\n    //\n    RelateBangumiResourceCard resource = 4;\n    //\n    RelateGameCard game = 5;\n    //\n    RelateCMCard cm = 6;\n    //\n    RelateLiveCard live = 7;\n    //\n    RelateBangumiAvCard bangumi_av = 8;\n    //\n    RelatedAICard ai_card = 9;\n    //\n    RelateBangumiUgcCard bangumi_ugc = 13;\n    //\n    RelateSpecial special = 14;\n  }\n  //\n  RelateThreePoint three_point = 10;\n  //\n  google.protobuf.Any cm_stock = 11;\n  //\n  CardBasicInfo basic_info = 12;\n}\n\n// 视频详情下方推荐卡子类型\nenum RelateCardType {\n  //\n  CARD_TYPE_UNKNOWN = 0;\n  //\n  AV = 1;\n  //\n  BANGUMI = 2;\n  //\n  RESOURCE = 3;\n  //\n  GAME = 4;\n  //\n  CM = 5;\n  //\n  LIVE = 6;\n  //\n  AI_RECOMMEND = 7;\n  //\n  BANGUMI_AV = 8;\n  //\n  BANGUMI_UGC = 9;\n  //\n  SPECIAL = 10;\n}\n\n// 视频详情下方推荐卡子类型: 广告推广\nmessage RelateCMCard {\n  //\n  int64 aid = 1;\n  //\n  google.protobuf.Any source_content = 2;\n  //\n  int64 duration = 3;\n  //\n  Stat stat = 4;\n}\n\n// 视频详情下方推荐配置\nmessage RelateConfig {\n  //\n  int64 valid_show_m = 1;\n  //\n  int64 valid_show_n = 2;\n  //\n  bilibili.pagination.Pagination pagination = 3;\n  //\n  bool can_load_more = 4;\n}\n\n// 视频详情下方推荐卡子类型: AI 推荐?\nmessage RelatedAICard {\n  //\n  int64 aid = 1;\n  //\n  int64 duration = 2;\n  //\n  Staff up_info = 3;\n  //\n  Stat stat = 4;\n  //\n  map<string, string> report = 5;\n  //\n  string goto_type = 6;\n}\n\n// 视频详情下方推荐卡子类型: 点击不喜欢后占位卡片\nmessage RelateDislike {\n  //\n  string title = 1;\n  //\n  string sub_title = 2;\n  //\n  string closed_sub_title = 3;\n  //\n  string paste_text = 4;\n  //\n  string closed_paste_text = 5;\n  //\n  repeated DislikeReasons dislike_reason = 6;\n  //\n  string toast = 7;\n  //\n  string closed_toast = 8;\n}\n\n// 视频详情下方推荐卡子类型: 游戏推广\nmessage RelateGameCard {\n  //\n  int64 reserve_status = 1;\n  //\n  string reserve_status_text = 2;\n  //\n  string reserve = 3;\n  //\n  float rating = 4;\n  //\n  string tag_name = 5;\n  //\n  RankInfo rank_info = 6;\n  //\n  Button pack_info = 7;\n  //\n  Button notice = 8;\n  //\n  PowerIconStyle power_icon_style = 9;\n  //\n  string game_rcmd_reason = 10;\n  //\n  WikiInfo wiki_info = 11;\n  //\n  BadgeInfo badge = 12;\n}\n\n//\nmessage RelateItem {\n  //\n  string url = 1;\n  //\n  string cover = 2;\n  //\n  bool use_default_browser = 3;\n}\n\n// 视频详情下方推荐卡子类型: 直播\nmessage RelateLiveCard {\n  //\n  int64 icon_type = 1;\n  //\n  string area_name = 2;\n  //\n  int64 watched_show = 3;\n  //\n  int64 live_status = 4;\n}\n\n// 视频下方推荐区\nmessage Relates {\n  //\n  repeated RelateCard cards = 1;\n  //\n  RelateConfig config = 2;\n}\n\n// 视频详情下方推荐卡子类型: 其他特殊\nmessage RelateSpecial {\n  //\n  BadgeInfo badge = 1;\n  //\n  BadgeInfo rcmd_reason = 2;\n}\n\n// 视频详情下方推荐卡右上角三点的内容\nmessage RelateThreePoint {\n  //\n  RelateDislike dislike = 1;\n  //\n  RelateDislike feedback = 2;\n  //\n  bool watch_later = 3;\n  //\n  string dislike_report_data = 4;\n}\n\n//\nenum ReserveBizType {\n  //\n  BizTypeNone = 0;\n  //\n  BizTypeReserveActivity = 1;\n  //\n  BizTypeFavSeason = 2;\n}\n\n//\nmessage ReserveButton {\n  //\n  bool status = 1;\n  //\n  string text = 3;\n  //\n  string selected_text = 4;\n  //\n  ReserveBizType order_type = 7;\n  //\n  oneof order_param {\n    //\n    BizReserveActivityParam reserve = 8;\n    //\n    BizFavParam fav = 9;\n  }\n}\n\n//\nmessage Rights {\n  //\n  int32 allow_download = 1;\n  //\n  int32 allow_review = 2;\n  //\n  int32 can_watch = 3;\n  //\n  string resource = 4;\n  //\n  int32 allow_dm = 5;\n  //\n  int32 allow_demand = 6;\n  // 区域限制\n  int32 area_limit = 7;\n}\n\n//\nmessage SeasonHead {\n  //\n  string title = 1;\n  //\n  string intro = 2;\n  //\n  StatInfo vt = 3;\n  //\n  StatInfo danmaku = 4;\n}\n\n//\nmessage SeasonShow {\n  //\n  string button_text = 1;\n  //\n  string join_text = 2;\n  //\n  string rule_text = 3;\n  //\n  string checkin_text = 4;\n  //\n  string checkin_prompt = 5;\n}\n\nenum SeasonType {\n  //\n  Unknown = 0;\n  //\n  Base = 1;\n  //\n  Good = 2;\n}\n\n//\nmessage SectionData {\n  //\n  int32 id = 1;\n  //\n  int32 section_id = 2;\n  //\n  string title = 3;\n  //\n  int32 can_ord_desc = 4;\n  //\n  string more = 5;\n  //\n  repeated int32 episode_ids = 6;\n  //\n  repeated ViewEpisode episodes = 7;\n  //\n  string split_text = 8;\n  //\n  Style module_style = 9;\n  //\n  string more_bottom_desc = 10;\n  //\n  repeated SerialSeason seasons = 11;\n  //\n  Button more_left = 12;\n  //\n  int32 type = 13;\n  //\n  map<string, string> report = 14;\n}\n\n//\nmessage SerialSeason {\n  //\n  int32 season_id = 1;\n  //\n  string title = 2;\n  //\n  string season_title = 3;\n  //\n  int32 is_new = 4;\n  //\n  string cover = 5;\n  //\n  string badge = 6;\n  //\n  int32 badge_type = 7;\n  //\n  BadgeInfo badge_info = 8;\n  //\n  string link = 9;\n  //\n  string resource = 10;\n  //\n  NewEp new_ep = 11;\n}\n\nenum SerialSeasonCoverStyle {\n  //\n  TITLE = 0;\n  //\n  PICTURE = 1;\n}\n\n//\nmessage SkipRange {\n  //\n  int32 start = 1;\n  //\n  int32 end = 2;\n}\n\n//\nmessage Sponsor {\n  //\n  int64 total = 1;\n  //\n  int64 week = 2;\n  //\n  repeated SponsorRank rank_list = 3;\n  //\n  Mine mine = 4;\n  //\n  PointActivity point_activity = 5;\n  //\n  repeated Pendant pendants = 6;\n  //\n  repeated Threshold threshold = 7;\n}\n\n//\nmessage SponsorRank {\n  //\n  int64 uid = 1;\n  //\n  string msg = 2;\n  //\n  string uname = 3;\n  //\n  string face = 4;\n  //\n  Vip vip = 5;\n}\n\n//\nmessage Staff {\n  //\n  int64 mid = 1;\n  //\n  int32 attention = 2;\n  //\n  string title = 3;\n  //\n  string name = 4;\n  //\n  string face = 5;\n  //\n  OfficialVerify official = 6;\n  //\n  Vip vip = 7;\n  //\n  int32 label_style = 8;\n  //\n  string fans = 9;\n}\n\n//\nmessage Staffs {\n  //\n  repeated Staff staff = 1;\n  //\n  string title = 2;\n}\n\n//\nmessage Stat {\n  // 视频观看时长\n  StatInfo vt = 1;\n  // 弹幕\n  StatInfo danmaku = 2;\n  // 回复数\n  int64 reply = 3;\n  // 收藏数\n  int64 fav = 4;\n  // 硬币数\n  int64 coin = 5;\n  // 分享数\n  int64 share = 6;\n  // 点赞数\n  int64 like = 7;\n  // 关注数\n  int64 follow = 8;\n}\n\n//\nmessage StatInfo {\n  //\n  int64 value = 1;\n  //\n  string text = 2;\n  //\n  string pure_text = 3;\n  //\n  string icon = 4;\n}\n\n//\nmessage Style {\n  //\n  int32 line = 1;\n  //\n  int32 hidden = 2;\n  //\n  repeated string show_pages = 3;\n}\n\n//\nmessage Tag {\n  //\n  int64 tag_id = 1;\n  //\n  string name = 2;\n  //\n  string uri = 3;\n  //\n  string tag_type = 4;\n}\n\n//\nmessage TheatreHotTopic {\n  //\n  int64 theatre_id = 1;\n  //\n  int64 theatre_set_id = 2;\n  //\n  string theatre_title = 3;\n  //\n  string background_image_url = 4;\n  //\n  string theatre_url = 5;\n  //\n  int64 hot_topic_id = 6;\n  // Original one is hottopicsetid, here renamed\n  int64 hot_topic_set_id = 7;\n  // Original one is hottopictitle, here renamed\n  string hot_topic_title = 8;\n  //\n  string hot_topic_url = 9;\n  //\n  int32 is_subscribe = 10;\n  //\n  map<string, string> report = 11;\n}\n\n//\nmessage Threshold {\n  //\n  int32 bp = 1;\n  //\n  int32 days = 2;\n  //\n  string days_text = 3;\n}\n\n//\nmessage TitleDeliveryButton {\n  //\n  string icon = 1;\n  //\n  string title = 2;\n  //\n  string link = 3;\n  //\n  map<string, string> report = 4;\n}\n\n//\nmessage UgcEpisode {\n  //\n  int64 id = 1;\n  //\n  int64 aid = 2;\n  //\n  int64 cid = 3;\n  //\n  string title = 4;\n  //\n  string cover = 5;\n  //\n  string cover_right_text = 6;\n  //\n  Page page = 7;\n  //\n  StatInfo vt = 8;\n  //\n  StatInfo danmaku = 9;\n}\n\n//\nmessage UgcIntroduction {\n  //\n  repeated Tag tags = 1;\n  //\n  Rating rating = 2;\n  //\n  Rank rank = 3;\n  //\n  repeated ViewMaterial bgm = 4;\n  //\n  repeated ViewMaterial sticker = 5;\n  //\n  repeated ViewMaterial video_source = 6;\n  //\n  int64 pubdate = 7;\n  //\n  repeated DescV2 desc = 8;\n}\n\n//\nmessage UgcSeasonActivity {\n  //\n  int32 type = 1;\n  //\n  int64 oid = 2;\n  //\n  int64 activity_id = 3;\n  //\n  string title = 4;\n  //\n  string intro = 5;\n  //\n  int32 day_count = 6;\n  //\n  int32 user_count = 7;\n  //\n  int64 join_deadline = 8;\n  //\n  int64 activity_deadline = 9;\n  //\n  int32 checkin_view_time = 10;\n  //\n  bool new_activity = 11;\n  //\n  UserActivity user_activity = 12;\n  //\n  SeasonShow season_show = 13;\n}\n\n//\nmessage UgcSeasons {\n  //\n  int64 id = 1;\n  //\n  string title = 2;\n  //\n  string cover = 3;\n  //\n  string supernatant_title = 4;\n  //\n  repeated UgcSection section = 5;\n  //\n  string union_title = 6;\n  //\n  SeasonHead head = 7;\n  //\n  int64 ep_count = 8;\n  //\n  int32 season_type = 9;\n  //\n  UgcSeasonActivity activity = 10;\n  //\n  repeated string season_ability = 11;\n  //\n  string season_title = 12;\n}\n\n//\nmessage UgcSection {\n  //\n  int64 id = 1;\n  //\n  string title = 2;\n  //\n  int64 type = 3;\n  //\n  repeated UgcEpisode episodes = 4;\n}\n\n//\nmessage UpLikeImg {\n  //\n  string pre_img = 1;\n  //\n  string suc_img = 2;\n  //\n  string content = 3;\n  //\n  int64 type = 4;\n}\n\n//\nmessage User {\n  //\n  int64 mid = 1;\n  //\n  string name = 2;\n  //\n  string face = 3;\n  //\n  int64 follower = 4;\n}\n\n//\nmessage UserActivity {\n  //\n  int32 user_state = 1;\n  //\n  int64 last_checkin_date = 2;\n  //\n  int32 checkin_today = 3;\n  //\n  int32 user_day_count = 4;\n  //\n  int32 user_view_time = 5;\n  //\n  string portrait = 6;\n}\n\n//\nmessage UserList {\n  //\n  repeated User list = 1;\n  //\n  string title = 2;\n}\n\n//\nmessage UserStatus {\n  //\n  int32 show = 1;\n  //\n  int32 follow = 2;\n}\n\n//\nmessage ViewEpisode {\n  //\n  int64 ep_id = 1;\n  //\n  string badge = 2;\n  //\n  int32 badge_type = 3;\n  //\n  BadgeInfo badge_info = 4;\n  //\n  int32 duration = 5;\n  //\n  int32 status = 6;\n  //\n  string cover = 7;\n  //\n  int64 aid = 8;\n  //\n  string title = 9;\n  //\n  string movie_title = 10;\n  //\n  string subtitle = 11;\n  //\n  string long_title = 12;\n  //\n  string toast_title = 13;\n  //\n  int64 cid = 14;\n  //\n  string from = 15;\n  //\n  string share_url = 16;\n  //\n  string share_copy = 17;\n  //\n  string short_link = 18;\n  //\n  string vid = 19;\n  //\n  string release_date = 20;\n  //\n  Dimension dimension = 21;\n  //\n  Rights rights = 22;\n  //\n  Interaction interaction = 23;\n  //\n  string bvid = 24;\n  //\n  int32 archive_attr = 25;\n  //\n  string link = 26;\n  //\n  string link_type = 27;\n  //\n  string bmid = 28;\n  //\n  int64 pub_time = 29;\n  //\n  int32 pv = 30;\n  //\n  int32 ep_index = 31;\n  //\n  int32 section_index = 32;\n  //\n  repeated Staff up_infos = 33;\n  //\n  Staff up_info = 34;\n  //\n  string dialog_type = 35;\n  //\n  string toast_type = 36;\n  //\n  repeated MultiViewEp multi_view_eps = 37;\n  //\n  bool is_sub_view = 38;\n  //\n  bool is_view_hide = 39;\n  //\n  string jump_link = 40;\n  //\n  Stat stat_for_unity = 41;\n  //\n  map<string, string> report = 42;\n}\n\n//\nmessage ViewMaterial {\n  //\n  int64 oid = 1;\n  //\n  int64 mid = 2;\n  //\n  string title = 3;\n  //\n  string author = 4;\n  //\n  string jump_url = 5;\n}\n\n//\nmessage Vip {\n  //\n  int32 type = 1;\n  //\n  int32 vip_status = 2;\n  //\n  int32 theme_type = 3;\n  //\n  VipLabel label = 4;\n  //\n  int32 is_vip = 5;\n}\n\n//\nmessage VipLabel {\n  //\n  string path = 1;\n  //\n  string text = 2;\n  //\n  string label_theme = 3;\n}\n\n//\nmessage WikiInfo {\n  //\n  string wiki_label = 1;\n  //\n  string wiki_url = 2;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/viewunite/pgcanymodel.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.viewunite.pgcanymodel;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/viewunite/common.proto\";\n\n//\nmessage Earphone {\n  //\n  string product_model = 1;\n  //\n  string like_toast_text = 2;\n  //\n  string switch_toast_text = 3;\n  //\n  string like_toast_voice = 4;\n}\n\n//\nmessage EarphoneConf {\n  //\n  repeated Earphone sp_phones = 1;\n}\n\n//\nmessage MultiViewInfo {\n  //\n  bool is_multi_view_season = 1;\n  //\n  string changing_dance = 2;\n}\n\n//\nmessage OgvData {\n  //\n  int32 media_id = 1;\n  //\n  int64 season_id = 2;\n  //\n  int32 season_type = 3;\n  //\n  int32 show_season_type = 4;\n  //\n  Rights rights = 5;\n  //\n  UserStatus user_status = 6;\n  //\n  int64 aid = 7;\n  //\n  Stat stat = 8;\n  //\n  int32 mode = 9;\n  //\n  Publish publish = 10;\n  //\n  PlayStrategy play_strategy = 11;\n  //\n  MultiViewInfo multi_view_info = 12;\n  //\n  OgvSwitch ogv_switch = 13;\n  //\n  int32 total_ep = 14;\n  //\n  bilibili.app.viewunite.common.NewEp new_ep = 15;\n  //\n  Reserve reserve = 16;\n  //\n  int32 status = 17;\n  //\n  repeated PlayFloatLayerActivity activity_float_layer = 18;\n  //\n  EarphoneConf earphone_conf = 19;\n  //\n  string cover = 20;\n  //\n  string square_cover = 21;\n  //\n  string share_url = 22;\n  //\n  string short_link = 23;\n  //\n  string title = 24;\n  //\n  string horizontal_cover169 = 25;\n  //\n  string horizontal_cover1610 = 26;\n  //\n  int32 has_can_play_ep = 27;\n}\n\n//\nmessage OgvSwitch {\n  //\n  int32 reduce_short_title_spacing = 1;\n  //\n  int32 merge_position_section_for_cinema = 2;\n  //\n  int32 merge_preview_section = 3;\n  //\n  int32 enable_show_vt_info = 4;\n}\n\n// 播放器浮层广告(?)\nmessage PlayFloatLayerActivity {\n  //\n  int32 id = 1;\n  //\n  string title = 2;\n  //\n  int32 type = 3;\n  //\n  int32 ad_badge_type = 4;\n  //\n  string link = 5;\n  //\n  string pic_url = 6;\n  //\n  string pic_anima_url = 7;\n  //\n  bilibili.app.viewunite.common.BadgeInfo badge = 8;\n  //\n  int64 show_rate_time = 9;\n}\n\n//\nmessage PlayStrategy {\n  //\n  repeated string strategies = 1;\n  //\n  int32 recommend_show_strategy = 2;\n  //\n  string auto_play_toast = 3;\n}\n\n//\nmessage Publish {\n  //\n  string pub_time = 1;\n  //\n  string pub_time_show = 2;\n  //\n  int32 is_started = 3;\n  //\n  int32 is_finish = 4;\n  //\n  int32 weekday = 5;\n  //\n  string release_date_show = 6;\n  //\n  string time_length_show = 7;\n  //\n  int32 unknow_pub_date = 8;\n  //\n  string update_info_desc = 9;\n}\n\n//\nmessage Reserve {\n  //\n  repeated bilibili.app.viewunite.common.ViewEpisode episodes = 1;\n  //\n  string tip = 2;\n}\n\n// 权限相关信息\nmessage Rights {\n  //\n  int32 allow_download = 1;\n  //\n  int32 allow_review = 2;\n  //\n  int32 can_watch = 3;\n  //\n  int32 is_cover_show = 4;\n  //\n  string copyright = 5;\n  //\n  string copyright_name = 6;\n  //\n  int32 allow_bp = 7;\n  //\n  int32 area_limit = 8;\n  //\n  int32 is_preview = 9;\n  //\n  int32 ban_area_show = 10;\n  //\n  int32 watch_platform = 11;\n  //\n  int32 allow_bp_rank = 12;\n  //\n  string resource = 13;\n  //\n  int32 forbid_pre = 14;\n  //\n  int32 only_vip_download = 15;\n  //\n  int32 new_allow_download = 16;\n}\n\n//\nmessage Stat {\n  //\n  string followers = 1;\n  //\n  bilibili.app.viewunite.common.StatInfo play_data = 2;\n}\n\n//\nmessage UserStatus {\n  //\n  int32 show = 1;\n  //\n  int32 follow = 2;\n  //\n  int32 follow_status = 3;\n  //\n  int32 pay = 4;\n  //\n  int32 sponsor = 5;\n  //\n  int32 vip = 6;\n  // vip 是否被冻结\n  int32 vip_frozen = 7;\n  //\n  WatchProgress watch_progress = 8;\n}\n\n// \nmessage ViewPgcAny {\n  //\n  OgvData ogv_data = 1;\n  //\n  map<int64, bilibili.app.viewunite.common.Staff> all_up_info = 2;\n}\n\n//\nmessage WatchProgress {\n  //\n  int64 last_ep_id = 1;\n  //\n  string last_ep_index = 2;\n  //\n  int64 last_time = 3;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/viewunite/ugcanymodel.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.viewunite.ugcanymodel;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/viewunite/common.proto\";\n\n//\nmessage Dislike {\n  //\n  string title = 1;\n  //\n  string subtitle = 2;\n  //\n  repeated DislikeReason reasons = 3;\n}\n\n//\nmessage DislikeReason {\n  //\n  int64 id = 1;\n  //\n  int64 mid = 2;\n  //\n  int32 rid = 3;\n  //\n  int64 tag_id = 4;\n  //\n  string name = 5;\n}\n\n//\nmessage ElecRank {\n  //\n  repeated ElecRankItem list = 1;\n  //\n  int64 count = 2;\n  //\n  string text = 3;\n}\n\n//\nmessage ElecRankItem {\n  //\n  string avatar = 1;\n  //\n  string nickname = 2;\n  //\n  string message = 3;\n  //\n  int64 mid = 4;\n}\n\n//\nmessage Premiere {\n  //\n  PremiereState premiere_state = 1;\n  //\n  int64 start_time = 2;\n  //\n  int64 service_time = 3;\n  //\n  int64 room_id = 4;\n}\n\n//\nmessage PremiereReserve {\n  //\n  int64 reserve_id = 1;\n  //\n  int64 count = 2;\n  //\n  bool is_follow = 3;\n}\n\n//\nmessage PremiereResource {\n  //\n  Premiere premiere = 1;\n  //\n  PremiereReserve reserve = 2;\n  //\n  PremiereText text = 3;\n}\n\nenum PremiereState {\n  //\n  premiere_none = 0;\n  //\n  premiere_before = 1;\n  //\n  premiere_in = 2;\n  //\n  premiere_after = 3;\n}\n\n//\nmessage PremiereText {\n  //\n  string title = 1;\n  //\n  string subtitle = 2;\n  //\n  string online_text = 3;\n  //\n  string online_icon = 4;\n  //\n  string online_icon_dark = 5;\n  //\n  string intro_title = 6;\n  //\n  string intro_icon = 7;\n  //\n  string guidance_pulldown = 8;\n  //\n  string guidance_entry = 9;\n  //\n  string intro_icon_night = 10;\n}\n\n//\nmessage ViewUgcAny {\n  //\n  PremiereResource premiere = 1;\n  //\n  Dislike dislike = 2;\n  //\n  string short_link = 3;\n  //\n  string share_subtitle = 4;\n  //\n  repeated bilibili.app.viewunite.common.Page pages = 5;\n  //\n  ElecRank elec_rank = 6;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/viewunite/v1/viewunite.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.viewunite.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\nimport \"bilibili/app/viewunite/common.proto\";\nimport \"bilibili/pagination/pagination.proto\";\nimport \"google/protobuf/any.proto\";\n\n// 统一视频信息接口 (7.41.0+)\nservice View {\n  //\n  rpc ArcRefresh(ArcRefreshReq) returns (ArcRefreshReply);\n  // 视频详情页下方推荐\n  rpc RelatesFeed(RelatesFeedReq) returns (RelatesFeedReply);\n  //\n  rpc View(ViewReq) returns (ViewReply);\n  //\n  rpc ViewProgress(ViewProgressReq) returns (ViewProgressReply);\n}\n\n//\nmessage ActivityResource {\n  //\n  string dark_text_color = 1;\n  //\n  string divider_color = 2;\n  //\n  string bg_color = 3;\n  //\n  string selected_bg_color = 4;\n  //\n  string text_color = 5;\n  //\n  string light_text_color = 6;\n}\n\n// 业务信息\nmessage Arc {\n  //\n  int64 aid = 1;\n  //\n  int64 cid = 2;\n  //\n  int64 duration = 3;\n  //\n  bilibili.app.viewunite.common.Stat stat = 4;\n  //\n  string bvid = 5;\n  //\n  int32 copyright = 6;\n  //\n  Rights right = 7;\n  //\n  string cover = 8;\n  //\n  int64 type_id = 9;\n  //\n  string title = 10;\n}\n\n//\nmessage ArcRefreshReply {\n  //\n  bilibili.app.viewunite.common.Stat stat = 1;\n  //\n  SimpleReqUser req_user = 2;\n  //\n  SimpleArc arc = 3;\n  //\n  Online online = 4;\n  //\n  LikeConfig like_config = 5;\n}\n\n//\nmessage ArcRefreshReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n}\n\n//\nmessage AttentionCard {\n  //\n  repeated ShowTime show_time = 1;\n}\n\n//\nmessage BizFollowVideoParam {\n  //\n  int64 season_id = 1;\n}\n\n//\nmessage BizJumpLinkParam {\n  //\n  string url = 1;\n}\n\n//\nmessage BizReserveActivityParam {\n  //\n  int64 activity_id = 1;\n  //\n  string from = 2;\n  //\n  string type = 3;\n  //\n  int64 oid = 4;\n  //\n  int64 reserve_id = 5;\n}\n\n//\nmessage BizReserveGameParam {\n  //\n  int64 game_id = 1;\n}\n\nenum BizType {\n  //\n  BizTypeNone = 0;\n  //\n  BizTypeFollowVideo = 1;\n  //\n  BizTypeReserveActivity = 2;\n  //\n  BizTypeJumpLink = 3;\n  //\n  BizTypeFavSeason = 4;\n  //\n  BizTypeReserveGame = 5;\n}\n\n//\nmessage Button {\n  //\n  string title = 1;\n  //\n  string uri = 2;\n  //\n  string icon = 3;\n  //\n  JumpShowType jump_show_type = 4;\n}\n\n//\nmessage ChargingPlus {\n  //\n  bool pass = 1;\n  //\n  repeated PlayToast play_toast = 2;\n}\n\n//\nmessage Chronos {\n  //\n  string md5 = 1;\n  //\n  string file = 2;\n  //\n  string sign = 3;\n}\n\n//\nmessage ChronosParam {\n  //\n  string engine_version = 1;\n  //\n  string message_protocol = 2;\n  //\n  string service_key = 3;\n}\n\n// 推广信息\nmessage CM {\n  //\n  google.protobuf.Any cm_under_player = 1;\n  //\n  google.protobuf.Any ads_control = 2;\n  //\n  repeated google.protobuf.Any source_content = 3;\n}\n\n//\nmessage CommandDm {\n  //\n  int64 id = 1;\n  //\n  int64 oid = 2;\n  //\n  int64 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  //\n  string extra = 9;\n  //\n  string idstr = 10;\n}\n\n// 播放器配置\nmessage Config {\n  //\n  Online online = 1;\n  //\n  PlayerIcon player_icon = 2;\n  //\n  StoryEntrance story_entrance = 3;\n}\n\n// 视频播放时弹出的卡片\nmessage ContractCard {\n  // 在第几秒弹出\n  float display_progress = 1;\n  //\n  int64 display_accuracy = 2;\n  // 弹出后停留的时间\n  int64 display_duration = 3;\n  // 展示方式, 暂未知对应关系\n  int32 show_mode = 4;\n  // 页面类型, 暂未知对应关系\n  int32 page_type = 5;\n  //\n  UpperInfos upper = 6;\n  //\n  int32 is_follow_display = 7;\n  // 卡片的文字说明信息\n  ContractText text = 8;\n  //\n  int64 follow_display_end_duration = 9;\n  //\n  int32 is_play_display = 10;\n  //\n  int32 is_interact_display = 11;\n}\n\n// 视频播放时弹出的卡片的文字说明信息\nmessage ContractText {\n  //\n  string title = 1;\n  //\n  string subtitle = 2;\n  //\n  string inline_title = 3;\n}\n\n//\nmessage Control {\n  //\n  bool limit = 1;\n}\n\n//\nmessage DmResource {\n  //\n  repeated CommandDm command_dms = 1;\n  //\n  AttentionCard attention = 2;\n  //\n  repeated OperationCard cards = 3;\n}\n\nenum ECode {\n  //\n  CODE_DEFAULT = 0;\n  //\n  CODE_404 = 1;\n  // 青少年限制\n  CODE_TEENAGER = 78301;\n}\n\n//\nmessage ECodeConfig {\n  //\n  string redirect_url = 1;\n}\n\n//\nmessage IconData {\n  //\n  string meta_json = 1;\n  //\n  string sprits_img = 2;\n}\n\n// 视频介绍 Tab\nmessage IntroductionTab {\n  //\n  string title = 1;\n  //\n  repeated bilibili.app.viewunite.common.Module modules = 2;\n}\n\nenum JumpShowType {\n  //\n  JST_DEFAULT = 0;\n  //\n  JST_FULLSCREEN = 1;\n  //\n  JST_HALFSCREEN = 2;\n}\n\n//\nmessage LikeConfig {\n  bilibili.app.viewunite.common.UpLikeImg triple_like = 1;\n  //\n  string like_animation = 2;\n}\n\n// 素材详情\nmessage Material {\n  //\n  string icon = 1;\n  //\n  string text = 2;\n  //\n  string url = 3;\n  //\n  MaterialBizType type = 4;\n  //\n  string param = 5;\n  //\n  string static_icon = 6;\n  //\n  string bg_color = 7;\n  //\n  string bg_pic = 8;\n  //\n  int32 jump_type = 9;\n  //\n  PageType page_type = 10;\n  //\n  bool need_login = 11;\n}\n\n// 素材类型\nenum MaterialBizType {\n  //\n  NONE = 0;\n  //\n  ACTIVITY = 1;\n  //\n  BGM = 2;\n  //\n  EFFECT = 3;\n  //\n  SHOOT_SAME = 4;\n  //\n  SHOOT_TOGETHER = 5;\n  //\n  ACTIVITY_ICON = 6;\n  //\n  NEW_BGM = 7;\n}\n\n// 素材来源\nenum MaterialSource {\n  //\n  DEFAULT = 0;\n  // 必剪素材\n  BIJIAN = 1;\n}\n\n//\nmessage Online {\n  //\n  bool online_show = 1;\n}\n\n//\nmessage OperationCard {\n  //\n  int64 id = 1;\n  //\n  int32 from = 2;\n  //\n  int32 to = 3;\n  //\n  bool status = 4;\n  //\n  BizType biz_type = 5;\n  //\n  OperationCardContent content = 6;\n  //\n  oneof param {\n    //\n    BizFollowVideoParam follow = 7;\n    //\n    BizReserveActivityParam reserve = 8;\n    //\n    BizJumpLinkParam jump = 9;\n    //\n    BizReserveGameParam game = 10;\n  }\n}\n\n//\nmessage OperationCardContent {\n  //\n  string title = 1;\n  //\n  string subtitle = 2;\n  //\n  string icon = 3;\n  //\n  string button_title = 4;\n  //\n  string button_selected_title = 5;\n  //\n  bool show_selected = 6;\n}\n\n//\nenum PageCategory {\n  //\n  COMMON_PAGE = 0;\n  //\n  ACTIVITY_PAGE = 1;\n}\n\n//\nmessage PageControl {\n  Control toast_show = 1;\n  Control material_show = 2;\n  Control up_show = 3;\n}\n\n// 页面类型\nenum PageType {\n  // H5页面(Webview)\n  H5 = 0;\n  // 原生页面(native)\n  NA = 1;\n}\n\n//\nmessage PlayerIcon {\n  //\n  string url1 = 1;\n  //\n  string hash1 = 2;\n  //\n  string url2 = 3;\n  //\n  string hash2 = 4;\n  //\n  string drag_left_png = 5;\n  //\n  string middle_png = 6;\n  //\n  string drag_right_png = 7;\n  //\n  IconData drag_data = 8;\n  //\n  IconData nodrag_data = 9;\n}\n\n//\nmessage PlayToast {\n  //\n  PlayToastEnum business = 1;\n  //\n  string icon_url = 2;\n  //\n  string text = 3;\n}\n\nenum PlayToastEnum {\n  //\n  PLAYTOAST_UNKNOWN = 0;\n  //\n  PLAYTOAST_CHARGINGPLUS = 1;\n}\n\n//\nmessage PointMaterial {\n  //\n  string url = 1;\n  //\n  MaterialSource material_source = 2;\n}\n\n//\nmessage Relate {\n  //\n  int64 device_type = 1;\n  //\n  bilibili.pagination.Pagination pagination = 2;\n}\n\n// 视频详情页下方推荐 Reply\nmessage RelatesFeedReply {\n  //\n  repeated bilibili.app.viewunite.common.RelateCard relates = 1;\n  //\n  bilibili.pagination.Pagination pagination = 2;\n}\n\n// 视频详情页下方推荐 Req\nmessage RelatesFeedReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n  //\n  string from = 3;\n  //\n  string spmid = 4;\n  //\n  string from_spmid = 5;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 6;\n  //\n  bilibili.pagination.Pagination pagination = 7;\n  //\n  string session_id = 8;\n  //\n  int64 auto_play = 9;\n  //\n  string from_track_id = 10;\n}\n\n//\nmessage ReplyStyle {\n  //\n  string badge_url = 1;\n  //\n  string badge_text = 2;\n  //\n  int64 badge_type = 3;\n}\n\n//\nmessage ReplyTab {\n  //\n  ReplyStyle reply_style = 1;\n  //\n  string title = 2;\n  //\n  TabControl control = 3;\n}\n\n//\nmessage ReqUser {\n  //\n  int32 favorite = 1;\n  //\n  int32 like = 2;\n  //\n  int32 coin = 3;\n  //\n  int32 fav_season = 4;\n  //\n  int32 follow = 5;\n  //\n  int32 dislike = 6;\n  // 头像旁充电按钮\n  Button elec_plus_btn = 7;\n  //\n  ChargingPlus charging_plus = 8;\n}\n\n//\nmessage Rights {\n  //\n  bool only_vip_download = 1;\n  //\n  bool no_reprint = 2;\n  //\n  bool download = 3;\n}\n\n//\nmessage ShowTime {\n  //\n  int32 start_time = 1;\n  //\n  int32 end_time = 2;\n  //\n  double pos_x = 3;\n  //\n  double pos_y = 4;\n}\n\n//\nmessage SimpleArc {\n  //\n  int32 copyright = 1;\n}\n\n//\nmessage SimpleReqUser {\n  //\n  int32 favorite = 1;\n  //\n  int32 like = 2;\n  //\n  int32 coin = 3;\n}\n\n//\nmessage StoryEntrance {\n  //\n  bool arc_play_story = 1;\n  //\n  string story_icon = 2;\n  //\n  bool arc_landscape_story = 3;\n  //\n  string landscape_icon = 4;\n  //\n  bool play_story = 5;\n}\n\n//\nmessage Tab {\n  //\n  repeated TabModule tab_module = 1;\n  //\n  string tab_bg = 2;\n  //\n  TabControl danmaku_entrance = 3;\n}\n\n// 评论区/弹幕 Tab 控制\nmessage TabControl {\n  //\n  bool limit = 1;\n  //\n  bool disable = 2;\n  //\n  string disable_click_tip = 3;\n}\n\n//\nmessage TabModule {\n  //\n  TabType tab_type = 1;\n  //\n  oneof tab {\n    //\n    IntroductionTab introduction = 2;\n    //\n    ReplyTab reply = 3;\n    //\n    bilibili.app.viewunite.common.ActivityTab activity_tab = 4;\n  }\n}\n\nenum TabType {\n  //\n  TAB_NONE = 0;\n  // 详情 Tab\n  TAB_INTRODUCTION = 1;\n  // 评论区 Tab\n  TAB_REPLY = 2;\n  // OGV 活动信息 Tab\n  TAB_OGV_ACTIVITY = 3;\n}\n\n//\nenum UnionType {\n  //\n  UGC = 0;\n  //\n  OGV = 1;\n}\n\n// UP主信息(可是Upper这个... 程序员英文不过关吧? )\nmessage UpperInfos {\n  // 粉丝数\n  uint64 fans_count = 1;\n  // 过去半年内的稿件数\n  uint64 arc_count_last_half_year = 2;\n  //\n  int64 first_up_dates = 3;\n  // UP稿件总播放数\n  uint64 total_play_count = 4;\n}\n\n//\nmessage VideoGuide {\n  //\n  repeated Material material = 1;\n  //\n  VideoViewPoint video_point = 2;\n  //\n  ContractCard contract_card = 3;\n}\n\n//\nmessage VideoPoint {\n  //\n  int32 type = 1;\n  //\n  int64 from = 2;\n  //\n  int64 to = 3;\n  //\n  string content = 4;\n  //\n  string cover = 5;\n  //\n  string logo_url = 6;\n}\n\n//\nmessage VideoShot {\n  //\n  string pv_data = 1;\n  //\n  int32 img_x_len = 2;\n  //\n  int32 imd_x_size = 3;\n  //\n  int32 img_y_len = 4;\n  //\n  int32 img_y_size = 5;\n  //\n  repeated string image = 6;\n}\n\n//\nmessage VideoViewPoint {\n  //\n  repeated VideoPoint points = 1;\n  //\n  PointMaterial point_material = 2;\n  //\n  bool point_permanent = 3;\n}\n\n//\nmessage ViewBase {\n  //\n  UnionType union_type = 1;\n  //\n  PageType page_type = 2;\n  //\n  PageControl control = 3;\n  //\n  ActivityResource activity_resource = 4;\n  //\n  Config config = 5;\n}\n\n//\nmessage ViewProgressReply {\n  //\n  VideoGuide video_guide = 1;\n  //\n  Chronos chronos = 2;\n  //\n  VideoShot arc_shot = 3;\n  //\n  DmResource dm = 4;\n}\n\n//\nmessage ViewProgressReq {\n  //\n  uint64 aid = 1;\n  //\n  uint64 cid = 2;\n  //\n  uint64 up_mid = 3;\n  //\n  ChronosParam  chronos_param = 4;\n  //\n  UnionType type = 5;\n}\n\n//\nmessage ViewReply {\n  //\n  ViewBase view_base = 1;\n  //\n  Arc arc = 2;\n  //\n  ReqUser req_user = 3;\n  //\n  bilibili.app.viewunite.common.Owner owner = 4;\n  //\n  Tab tab = 5;\n  //\n  google.protobuf.Any supplement = 6;\n  //\n  CM cm = 7;\n  //\n  ECode ecode = 8;\n  //\n  ECodeConfig ecode_config = 9;\n  //\n  map<string, string> report = 10;\n}\n\n//\nmessage ViewReq {\n  //\n  uint64 aid = 1;\n  //\n  string bvid = 2;\n  //\n  string from = 3;\n  //\n  string spmid = 4;\n  //\n  string from_spmid = 5;\n  //\n  string session_id = 6;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 7;\n  //\n  string track_id = 8;\n  //\n  map<string, string> extra_content = 9;\n  //\n  string play_mode = 10;\n  //\n  Relate relate = 11;\n  //\n  string biz_extra = 12;\n  //\n  string ad_extra = 13;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/app/wall/v1/wall.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.app.wall.v1;\n\noption java_multiple_files = true;\n\n// 免流规则\nservice Wall {\n  // 获取免流规则信息\n  rpc RuleInfo (RuleRequest) returns (RulesReply);\n}\n\n// 免流规则信息\nmessage RuleInfo {\n  // 是否支持免流\n  bool tf = 1;\n  // 操作模式\n  // break:无 replace:替换 proxy:代理\n  string m = 2;\n  // 操作参数\n  string a = 3;\n  // 匹配目标正则\n  string p = 4;\n  //\n  repeated string a_backup = 5;\n}\n\n// 获取免流规则信息-请求\nmessage RuleRequest {}\n\n// 免流规则信息组\nmessage RulesInfo {\n  // 免流规则信息\n  repeated RuleInfo rulesInfo = 1;\n}\n\n// 获取免流规则信息-响应\nmessage RulesReply {\n  // 各ISP的免流规则信息组\n  // ISP如: cu ct cm\n  map<string, RulesInfo> rulesInfo = 1;\n  //\n  string hash_value = 2;\n}\n\n\n\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/editor/notify.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.editor;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n//\nservice OperationNotify {\n  //\n  rpc OperationNotify(google.protobuf.Empty) returns (stream Notify);\n}\n\nmessage Notify {\n  // 消息唯一标示\n  int64   msg_id = 1;\n  // 消息类型\n  int32   msg_type = 2;\n  // 接收方uid\n  int64   receiver_uid = 3;\n  //接收方类型\n  int32   receiver_type = 4;\n  // 故事的版本\n  int64   story_version = 5;\n  // 操作结果的hash值\n  int64   op_hash = 6;\n  // 操作产生用户的uid\n  int64   op_sender = 7;\n  // patch内容\n  string  op_content = 8;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/esports/notify.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.esports;\n\noption java_multiple_files = true;\n\nmessage Notify {\n  // cid\n  int64 cid = 1;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/fission/notify.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.fission;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n//\nservice Fission {\n  //\n  rpc GameNotify(google.protobuf.Empty) returns (stream GameNotifyReply);\n}\n\nmessage GameNotifyReply {\n  // 类型字段\n  uint32 type = 1;\n  // 数据字段\n  string data = 2;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/im/notify.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.im;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n//\nservice Notify {\n  //\n  rpc WatchNotify(google.protobuf.Empty) returns (stream NotifyRsp);\n}\n\n//\nenum PLType {\n  //\n  EN_PAYLOAD_NORMAL = 0;\n  //\n  EN_PAYLOAD_BASE64 = 1;\n}\n\n//\nenum CmdId {\n  // 非法cmd\n  EN_CMD_ID_INVALID = 0;\n  // 服务端主动发起\n  EN_CMD_ID_MSG_NOTIFY = 1;\n  //\n  EN_CMD_ID_KICK_OUT = 2;\n}\n\n//\nmessage NotifyRsp {\n  //\n  uint64 uid = 1;\n  // 命令id\n  uint64 cmd = 2;\n  //\n  bytes payload = 3;\n  //\n  PLType payload_type = 4;\n}\n\n//\nmessage Msg {\n  // 发送方uid\n  uint64 sender_uid = 1;\n  // 接收方类型\n  int32  receiver_type = 2;\n  // 接收方id\n  uint64 receiver_id = 3;\n  // 客户端的序列id 用于服务端去重\n  uint64 cli_msg_id = 4;\n  // 消息类型\n  int32  msg_type = 5;\n  // 消息内容\n  string content = 6;\n  // 服务端的序列号\n  uint64 msg_seqno = 7;\n  // 消息发送时间（服务端时间）\n  uint64 timestamp = 8;\n  // at用户列表\n  repeated uint64 at_uids = 9;\n  // 多人消息\n  repeated uint64 recver_ids = 10;\n  // 消息唯一标示\n  uint64 msg_key = 11;\n  // 消息状态\n  uint32 msg_status = 12;\n  // 是否为系统撤销\n  bool sys_cancel = 13;\n  // 是否是多聊消息 目前群通知管理员的部分通知属于该类消息\n  uint32 is_multi_chat = 14;\n  // 表示撤回的消息的session_seqno 用以后续的比较 实现未读数的正确显示\n  uint64 withdraw_seqno = 15;\n  // 通知码\n  string notify_code = 16;\n  // 消息来源\n  uint32 msg_source = 17;\n}\n\n//\nmessage NotifyInfo {\n  //\n  uint32 msg_type = 1;\n  //\n  uint64 talker_id = 2;\n  //\n  uint32 session_type = 3;\n}\n\n//\nmessage ReqServerNotify {\n  // 最新序列号\n  uint64 lastest_seqno = 1;\n  // 即时消息 该类消息主要用于系统通知 当客户端sync msg时 不会sync到此类消息\n  Msg instant_msg = 2;\n  //\n  NotifyInfo notify_info = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/main/dm.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.main;\n\noption java_multiple_files = true;\n\n// 实时弹幕事件\nmessage DanmukuEvent {\n  // 弹幕列表\n  repeated DanmakuElem elems = 1;\n}\n\n// 弹幕条目\nmessage DanmakuElem {\n  // 弹幕dmid\n  int64 id = 1;\n  // 弹幕出现位置(单位为ms)\n  int32 progress = 2;\n  // 弹幕类型\n  int32 mode = 3;\n  // 弹幕字号\n  int32 fontsize = 4;\n  // 弹幕颜色\n  uint32 color = 5;\n  // 发送着mid hash\n  string mid_hash = 6;\n  // 弹幕正文\n  string content = 7;\n  // 发送时间\n  int64 ctime = 8;\n  // 弹幕动作\n  string action = 9;\n  // 弹幕池\n  int32 pool = 10;\n  // 弹幕id str\n  string id_str = 11;\n}\n\n// 互动弹幕\nmessage CommandDm {\n  // 弹幕id\n  int64 id = 1;\n  // 对象视频cid\n  int64 oid = 2;\n  // 发送者mid\n  int64 mid = 3;\n  //\n  int32 type = 4;\n  // 互动弹幕指令\n  string command = 5;\n  // 互动弹幕正文\n  string content = 6;\n  // 弹幕状态\n  int32 state = 7;\n  // 出现时间\n  int32 progress = 8;\n  // 创建时间\n  string ctime = 9;\n  // 发布时间\n  string mtime = 10;\n  // 扩展json数据\n  string extra = 11;\n  // 弹幕id str类型\n  string idStr = 12;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/main/native.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.main;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n//\nservice NativePage {\n  //\n  rpc WatchNotify(google.protobuf.Empty) returns (stream NativePageEvent);\n}\n\n//\nmessage NativePageEvent {\n  // Native页ID\n  int64 PageID = 1;\n  //\n  repeated EventItem Items = 2;\n}\n\n//\nmessage EventItem {\n  // 组件标识\n  int64 ItemID = 1;\n  // 组件类型\n  string Type = 2;\n  // 进度条数值\n  int64 Num = 3;\n  // 进度条展示数值\n  string DisplayNum = 4;\n  // h5的组件标识\n  string WebKey = 5;\n  // 活动统计维度\n  // 0:用户维度 1:规则维度\n  int64 dimension = 6;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/main/resource.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.main;\n\nimport \"google/protobuf/empty.proto\";\n\noption java_multiple_files = true;\n\n//\nservice Resource {\n  //\n  rpc TopActivity(google.protobuf.Empty) returns (stream TopActivityReply);\n}\n\n//\nmessage TopActivityReply {\n  // 当前生效的资源\n  TopOnline online = 1;\n  // 对online内容进行hash和上次结果一样则不重新加载\n  string hash = 2;\n}\n\n// 当前生效的资源\nmessage TopOnline {\n  // 活动类型\n  // 1:七日活动 2:后台配置\n  int32 type = 1;\n  // 图标\n  string icon = 2;\n  // 跳转链接\n  string uri = 3;\n  // 资源状态标识(后台配置)\n  string unique_id = 4;\n  // 动画资源\n  Animate animate = 5;\n  // 红点\n  RedDot red_dot = 6;\n  // 活动名称\n  string name = 7;\n  // 轮询间隔 单位秒\n  int64 interval = 8;\n}\n\n// 动画资源\nmessage Animate {\n  // 动效结束展示icon\n  string icon = 1;\n  // 7日活动动画\n  string json = 2;\n  // s10活动svg动画\n  string svg = 3;\n  // 循环次数(默认0不返回 表示无限循环)\n  int32 loop = 4;\n}\n\n// 红点\nmessage RedDot {\n  // 红点类型\n  // 1:纯红点 2:数字红点\n  int32 type = 1;\n  // 如果是数字红点 显示的数字\n  int32 number = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/main/search.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.main;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\nimport \"bilibili/app/dynamic/v2/dynamic.proto\";\n\nservice Search {\n  rpc ChatResultPush (google.protobuf.Empty) returns (stream ChatResult);\n}\n\n// \nmessage Bubble {\n  repeated bilibili.app.dynamic.v2.Paragraph paragraphs = 1;\n}\n\n// \nmessage ChatResult {\n  //\n  int32 code = 1;\n  //\n  string session_id = 2;\n  //\n  repeated Bubble bubble = 3;\n  //\n  string rewrite_word = 4;\n  //\n  string title = 5;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/note/sync.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.note;\n\noption java_multiple_files = true;\n\n//\nmessage Sync {\n  // 笔记id\n  int64 note_id = 1;\n  // 唯一标示\n  string hash = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/ogv/freya.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.ogv;\n\noption java_multiple_files = true;\n\n// 播放状态\nenum PlayStatus {\n  // 暂停\n  Pause = 0;\n  // 播放\n  Play = 1;\n  // 终止\n  End = 2;\n}\n\n// 房间类型\nenum RoomType {\n  // 私密\n  Private = 0;\n  // 公开\n  Open = 1;\n}\n\n// 信息通知发送领域\nenum MessageDomain {\n  // 默认\n  DefaultDomain = 0;\n  // 房间用户\n  RoomMid = 1;\n  // 系统通知\n  SystemInfo = 2;\n}\n\n// 通知信息类型\nenum MessageType {\n  // 默认\n  DefaultType = 0;\n  // 房间用户\n  ChatMessage = 1;\n  // 系统通知\n  SystemMessage = 2;\n}\n\n// 触发通知类型\nenum TriggerType {\n  // 默认\n  DefaultTrigger = 0;\n  // 关注、取消关注\n  Relation = 1;\n}\n\n// 房间人员变更事件\nmessage RoomMemberChangeEvent {\n  // 房间id\n  int64 room_id = 1;\n  // 房主id\n  int64 owner_id = 2;\n  // 房间成员列表\n  repeated UserInfoProto members = 3;\n  // 提示信息\n  MessageProto message = 4;\n}\n\n// 播放进度同步事件\nmessage ProgressSyncEvent {\n  // 房间id\n  int64 room_id = 1;\n  // 播放中的season_id\n  int64 season_id = 2;\n  // 播放中的episode_id\n  int64 episode_id = 3;\n  // 播放状态\n  PlayStatus status = 4;\n  // 房主播放进度\n  int64 progress = 5;\n  // 提示信息\n  MessageProto message = 6;\n}\n\n// 房间状态更新\nmessage RoomUpdateEvent {\n  // 房间id\n  int64 room_id = 1;\n  // 房间变更状态\n  RoomType type = 2;\n  // 提示信息\n  MessageProto message = 3;\n}\n\n// 房间销毁通知\nmessage RoomDestroyEvent {\n  // 房间id\n  int64 room_id = 1;\n  // 提示信息\n  MessageProto message = 4;\n}\n\n// 房间触发通知\nmessage RoomTriggerEvent {\n  // 操作人\n  int64 mid = 1;\n  // 提示信息\n  MessageProto message = 2;\n  // 触发类型\n  TriggerType trigger = 3;\n}\n\n//用户信息\nmessage UserInfoProto {\n  // 用户id\n  int64 mid = 1;\n  // 用户头像url\n  string face = 2;\n  // 昵称\n  string nickname = 3;\n  // 等级\n  int32 level = 4;\n  // 签名\n  string sign = 5;\n  // 大会员信息\n  VipProto vip = 6;\n  // 身份认证信息\n  OfficialProto official = 7;\n  // 挂件信息\n  PendantProto pendant = 8;\n  // 设备buvid\n  string buvid = 9;\n}\n\n//通知信息\nmessage MessageProto {\n  // 可带占位符匹配的消息体 ep \"还没有其他小伙伴，[去邀请>]<https://big.bilibili.com/mobile/giftIndex?mid=123>\"\n  string content = 1;\n  // 消息体类型\n  // 0:json格式的文本消息 1:支持全文本可点(破冰)\n  int32 content_type = 2;\n}\n\n//大会员信息\nmessage VipProto {\n  int32 type = 1;\n  int32 status = 2;\n  int64 due_date = 3;\n  int32 vip_pay_type = 4;\n  int32 theme_type = 5;\n  // 大会员角标\n  // 0:无角标 1:粉色大会员角标 2:绿色小会员角标\n  int32 avatar_subscript = 6;\n  // 昵称色值，可能为空，色值示例：#FFFB9E60\n  string nickname_color = 7;\n}\n\n//认证信息\nmessage OfficialProto {\n  int32 role = 1;\n  string title = 2;\n  string desc = 3;\n  int32 type = 4;\n}\n\n//挂件信息\nmessage PendantProto {\n  int32 pid = 1;\n  string name = 2;\n  string image = 3;\n  int64 expire = 4;\n  string image_enhance = 5;\n}\n\n// 通用信息通知\nmessage MessageEvent {\n  // 房间id\n  int64 room_id = 1;\n  // 消息id\n  int64 msg_id = 2;\n  // 消息发送服务端时间 时间戳 单位秒\n  int64 ts = 3;\n  // 信息通知发送主体id\n  int64 oid = 4;\n  // 信息通知发送领域\n  MessageDomain domain = 5;\n  // 通知信息类型\n  MessageType type = 6;\n  // 提示信息\n  MessageProto message = 7;\n  // 消息发送用户信息\n  UserInfoProto user = 8;\n  // 消息id str类型\n  string msg_id2 = 9;\n}\n\n// 聊天信息清除通知\nmessage RemoveChatEvent {\n  // 房间id\n  int64 room_id = 1;\n  // 撤回的聊天信息id\n  int64 msg_id = 2;\n  // 提示信息\n  MessageProto message = 3;\n}\n\n// \"一起看\"房间事件\nmessage FreyaEventBody {\n  // 房间id\n  int64 room_id = 1;\n  // 接收事件消息的白名单用户\n  repeated int64 white_mid = 2;\n  // 不处理信息的黑名单用户 优先级低于白名单 当白名单有数据时 忽略黑名单\n  repeated int64 ignore_mid = 3;\n  //命令类型\n  oneof event {\n    // 房间人员变更事件\n    RoomMemberChangeEvent member_change = 4;\n    // 播放进度同步事件\n    ProgressSyncEvent progress = 5;\n    // 房间状态更新\n    RoomUpdateEvent room_update = 6;\n    // 通用信息通知\n    MessageEvent message = 7;\n    // 聊天信息清除通知\n    RemoveChatEvent remove_chat = 8;\n    // 房间销毁通知\n    RoomDestroyEvent room_destroy = 9;\n    // 房间触发通知\n    RoomTriggerEvent room_trigger = 10;\n  }\n  // 消息序列号\n  int64 sequence_id = 100;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/ogv/live.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.ogv;\n\noption java_multiple_files = true;\n\n// 开播事件\nmessage LiveStartEvent {}\n\n// 直播中止事件\nmessage LiveEndEvent {}\n\n// 在线人数事件\nmessage LiveOnlineEvent {\n  //在线人数\n  int64 online = 1;\n}\n\n// 变更通知\nmessage LiveUpdateEvent {\n  // 直播后状态\n  // 1:下线 2:转点播\n  int32 after_premiere_type = 1;\n  // 直播开始绝对时间 单位ms\n  int64 start_time = 2;\n  // id\n  string id = 3;\n  // 服务端播放进度，未打散，负数表示距离开播时间，正数表示已开播时间，单位：毫秒\n  // 用户实际播放进度：progress - delay_time\n  int64 progress = 4;\n}\n\n// 直播间事件\nmessage CMDBody {\n  //命令类型\n  oneof event {\n    // 开播事件\n    LiveStartEvent start = 1;\n    // 直播中止事件\n    LiveEndEvent emergency = 2;\n    // 在线人数事件\n    LiveOnlineEvent online = 3;\n    // 变更通知\n    LiveUpdateEvent update = 4;\n  }\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/ticket/activitygame.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.ticket;\n\noption java_multiple_files = true;\n\n//\nenum RoomStatus {\n  // 暂停:\n  Pause = 0;\n  // 播放:\n  Play = 1;\n  // 终止:\n  End = 2;\n}\n\n// 推送选项\nmessage RoomEvent {\n  // RoomStatus 类型\n  RoomStatus room_status = 1;\n  //\n  string room_message = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/message/tv/proj.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.message.tv;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n//\nservice Tv {\n  // 投屏\n  rpc Proj(google.protobuf.Empty) returns (stream ProjReply);\n  // 直播状态\n  rpc LiveStatus(google.protobuf.Empty) returns (stream LiveStatusNotify);\n  // 赛事比分通知\n  rpc Esports(google.protobuf.Empty) returns (stream EsportsNotify);\n  // 直播插卡\n  rpc Publicity(google.protobuf.Empty) returns (stream PublicityNotify);\n  // 直转点\n  rpc LiveSkip(google.protobuf.Empty) returns (stream LiveSkipNotify);\n}\n\n// 投屏\nmessage ProjReply {\n  // 投屏命令\n  // 1:起播 2:快进 3:快退 4:seek播放进度 5:暂停 6:暂停恢复\n  int64 cmd_type = 1;\n  // 用户id\n  int64 mid = 2;\n  // 稿件id\n  int64 aid = 3;\n  // 视频id\n  int64 cid = 4;\n  // 视频类型\n  // 0:ugc 1:pgc 2:pugv\n  int64 video_type = 5;\n  // 单集id，pgc和pugv需要传\n  int64 ep_id = 6;\n  // 剧集id\n  int64 season_id = 7;\n  // seek 的位置，cmd位seek时有值，单位秒\n  int64 seek_ts = 8;\n  // 其他指令对应内容\n  string extra = 9;\n}\n\n// 直播状态\nmessage LiveStatusNotify {\n  // 直播状态\n  // 1:开播 2:关播 3:截流 4:截流恢复\n  int64 status = 1;\n  // 文案\n  string msg = 2;\n  // 直播房间号\n  int64 cid = 3;\n}\n\n//\nmessage EsportsNotify {\n  // 直播房间号\n  int64 cid = 1;\n}\n\n// 直播插卡\nmessage PublicityNotify {\n  // 插卡id\n  int64 publicity_id = 1;\n  // 直播房间号\n  int64 room_id = 2;\n  // 直播间状态\n  // 0:未开播 1:直播中 2:轮播中\n  int64 status = 3;\n}\n\n// 直转点\nmessage LiveSkipNotify {\n  // 直播id\n  int64 live_id = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/v1/broadcast.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/rpc/status.proto\";\nimport \"google/protobuf/any.proto\";\nimport \"google/protobuf/empty.proto\";\n\n// broadcast操作，对应每个target_path\nservice Broadcast {\n  // 用户鉴权\n  rpc Auth(AuthReq) returns (AuthResp);\n  // 心跳保活：成功心跳为4分45秒，重试心跳为30s，三次收不到进行重连（不超过5分45）\n  rpc Heartbeat(HeartbeatReq) returns (HeartbeatResp);\n  // 订阅target_path\n  rpc Subscribe(TargetPath) returns (google.protobuf.Empty);\n  // 取消订阅target_path\n  rpc Unsubscribe(TargetPath) returns (google.protobuf.Empty);\n  // 消息回执\n  rpc MessageAck(MessageAckReq) returns (google.protobuf.Empty);\n}\n\n// broadcast连接隧道\nservice BroadcastTunnel {\n  // 创建双向stream连接隧道\n  rpc CreateTunnel(stream BroadcastFrame) returns (stream BroadcastFrame);\n}\n\n//\nenum Action {\n  UNKNOWN = 0; //\n  UPDATE = 1;  //\n  DELETE = 2;  //\n}\n\n// 鉴权请求，通过authorization验证绑定用户mid\nmessage AuthReq {\n  // 冷启动id，算法uuid，重新起启会变\n  string guid = 1;\n  // 连接id，算法uuid，重连会变\n  string conn_id = 2;\n  // 最后收到的消息id，用于过虑重连后获取未读的消息\n  int64 last_msg_id = 3;\n}\n\n// 鉴权返回\nmessage AuthResp {\n\n}\n\n// target_path:\n//   \"/\" Service-Name \"/\" {method name} 参考 gRPC Request Path\nmessage BroadcastFrame {\n  // 请求消息信息\n  FrameOption options = 1;\n  // 业务target_path\n  string target_path = 2;\n  // 业务pb内容\n  google.protobuf.Any body = 3;\n}\n\n// message_id: \n//   client: 本次连接唯一的消息id，可用于回执\n//   server: 唯一消息id，可用于上报或者回执\n// sequence:\n//   client: 客户端应该每次请求时frame seq++，会返回对应的对称req/resp\n//   server: 服务端下行消息，只会返回默认值：0\nmessage FrameOption {\n  // 消息id\n  int64 message_id = 1;\n  // frame序号\n  int64 sequence = 2;\n  // 是否进行消息回执(发出MessageAckReq)\n  // downstream 上只有服务端设置为true，客户端响应\n  // upstream   上只有客户端设置为true，服务端响应\n  // 响应帧禁止设置is_ack，协议上禁止循环\n  // 通常只有业务帧才可能设置is_ack, 因为协议栈(例如心跳、鉴权)另有响应约定\n  bool is_ack = 3;\n  // 业务状态码\n  bilibili.rpc.Status status = 4;\n  // 业务ack来源, 仅downstream时候由服务端填写.\n  string ack_origin = 5;\n  //\n  int64 timestamp = 6;\n}\n\n// 心跳请求\nmessage HeartbeatReq{\n\n}\n\n// 心跳返回\nmessage HeartbeatResp{\n\n}\n\n// 消息回执\nmessage MessageAckReq {\n  // 消息id\n  int64 ack_id = 1;\n  // ack来源，由业务指定用于埋点跟踪\n  string ack_origin = 2;\n  // 消息对应的target_path，方便业务区分和监控统计\n  string target_path = 3;\n}\n\n// target_path\nmessage TargetPath {\n  // 需要订阅的target_paths\n  repeated string target_paths = 1;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/v1/laser.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n// Laser\nservice Laser {\n  // 监听上报事件\n  rpc WatchLogUploadEvent(google.protobuf.Empty) returns (stream LaserLogUploadResp);\n}\n\n// 服务端下发日志上报事件\nmessage LaserLogUploadResp {\n  // 任务id\n  int64 taskid = 1;\n  // 下发时间\n  string date = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/v1/mod.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n// ModManager\nservice ModManager {\n  //\n  rpc WatchResource(google.protobuf.Empty) returns (stream ModResourceResp);\n}\n\n//\nmessage ModResourceResp {\n  //\n  int32 atcion = 1;\n  //\n  string app_key = 2;\n  //\n  string pool_name = 3;\n  //\n  string module_name = 4;\n  //\n  int64 module_version = 5;\n  //\n  int64 list_version = 6;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/v1/push.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n// Push\nservice Push {\n  rpc WatchMessage(google.protobuf.Empty) returns (stream PushMessageResp);\n}\n\n//\nenum LinkType {\n  LINK_TYPE_UNKNOWN = 0; // 未知\n  LINK_TYPE_BANGUMI = 1; // 番剧\n  LINK_TYPE_VIDEO = 2;   // 视频\n  LINK_TYPE_LIVE = 3;    // 直播\n}\n\n//\nmessage PageBlackList {\n  //\n  string id = 1;\n}\n\n//\nmessage PageView {\n  //\n  string id = 1;\n}\n\n//\nmessage PushMessageResp {\n  // 业务类型\n  enum Biz {\n    // 未知\n    BIZ_UNKNOWN = 0;\n    // 视频\n    BIZ_VIDEO = 1;\n    // 直播\n    BIZ_LIVE = 2;\n    // 活动\n    BIZ_ACTIVITY = 3;\n  }\n  // 消息类型\n  enum Type {\n    // 未知\n    TYPE_UNKNOWN = 0;\n    // 默认\n    TYPE_DEFAULT = 1;\n    // 热门\n    TYPE_HOT = 2;\n    // 实时\n    TYPE_REALTIME = 3;\n    // 推荐\n    TYPE_RECOMMEND = 4;\n  }\n  // 展示未知\n  enum Position {\n    // 未知\n    POS_UNKNOWN = 0;\n    // 顶部\n    POS_TOP = 1;\n  }\n  // Deprecated: 推送任务id，使用string\n  int64 old_taskid = 1;\n  // 业务\n  // 1:是视频 2:是直播 3:是活动\n  Biz biz = 2;\n  // 类型\n  // 1:是默认 2:是热门 3:是实时 4:是推荐\n  Type type = 3;\n  // 主标题\n  string title = 4;\n  // 副标题\n  string summary = 5;\n  // 图片地址\n  string img = 6;\n  // 跳转地址\n  string link = 7;\n  // 展示位置，1是顶部\n  Position position = 8;\n  // 展示时长（单位：秒），默认3秒\n  int32 duration = 9;\n  // 失效时间\n  int64 expire = 10;\n  // 推送任务id\n  string taskid = 11;\n  // 应用内推送黑名单\n  // UGC:     ugc-video-detail\n  // PGC:     pgc-video-detail\n  // 一起看:   pgc-video-detail-theater\n  // 直播:     live-room-detail\n  // Story:    ugc-video-detail-vertical\n  // 播单黑名单 playlist-video-detail\n  repeated PageBlackList page_blackList = 12;\n  // 预留pvid\n  repeated PageView page_view = 13;\n  // 跳转资源\n  TargetResource target_resource = 14;\n  //\n  int32 image_frame = 15;\n  //\n  int32 image_marker = 16;\n  //\n  int32 image_position = 17;\n  //\n  int64 job = 18;\n}\n\n//\nmessage TargetResource {\n  //直播:   roomid\n  //UGC:   avid\n  //PGC:   seasonid\n  //Story: avid\n  //举个例子\n  //Type: LINK_TYPE_BANGUMI (番剧)\n  //Resource: {\"seasonid\":\"123\"}\n  //\n  //Type: LINK_TYPE_VIDEO (视频)\n  //Resource: {\"avid\":\"123\"}\n  //\n  //Type: LINK_TYPE_LIVE (直播)\n  //Resource: {\"roomid\":\"123\"}\n  //\n  LinkType Type = 1;\n  //\n  map<string, string> Resource = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/v1/room.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/rpc/status.proto\";\nimport \"google/protobuf/any.proto\";\n\n//\nservice BroadcastRoom {\n  //\n  rpc Enter(stream RoomReq) returns (stream RoomResp);\n}\n\n//\nmessage RoomErrorEvent {\n  //\n  bilibili.rpc.Status status = 1;\n}\n\n//\nmessage RoomJoinEvent {\n\n}\n\n//\nmessage RoomLeaveEvent {\n\n}\n\n//\nmessage RoomMessageEvent {\n  //\n  string target_path = 1;\n  //\n  google.protobuf.Any body = 2;\n}\n\n//\nmessage RoomOnlineEvent {\n  //\n  int32 online = 1;\n  //\n  int32 all_online = 2;\n}\n\n//\nmessage RoomReq {\n  // {type}://{room_id}\n  string id = 1;\n  oneof event {\n    //\n    RoomJoinEvent join = 2;\n    //\n    RoomLeaveEvent leave = 3;\n    //\n    RoomOnlineEvent online = 4;\n    //\n    RoomMessageEvent msg = 5;\n  }\n}\n\n//\nmessage RoomResp {\n  // {type}://{room_id}\n  string id = 1;\n  oneof event {\n    //\n    RoomJoinEvent join = 2;\n    //\n    RoomLeaveEvent leave = 3;\n    //\n    RoomOnlineEvent online = 4;\n    //\n    RoomMessageEvent msg = 5;\n    //\n    RoomErrorEvent err = 6;\n  }\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/v1/test.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/any.proto\";\nimport \"google/protobuf/empty.proto\";\n\n// 服务端下发的测试专用消息，客户端debug/release包都会通过弹窗响应该消息\n// 后端平台 必须 限制该消息只能针对单个用户发送\n\n// Test\nservice Test {\n  // 监听上报事件\n  rpc WatchTestEvent(google.protobuf.Empty) returns (stream TestResp);\n}\n\n//\nservice Test2 {\n  //\n  rpc Test(AddParams) returns (google.protobuf.Empty);\n}\n\n//\nmessage AddParams {\n  //\n  int32 a = 1;\n  //\n  int32 b = 2;\n}\n\n//\nmessage AddResult {\n  //\n  int32 r = 1;\n}\n\nmessage TestResp {\n  // 任务id\n  int64 taskid = 1;\n  // 时间戳\n  int64 timestamp = 2;\n  // 消息\n  string message = 3;\n  // 扩展\n  google.protobuf.Any extra = 4;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/broadcast/v2/laser.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.broadcast.v2;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n// Laser\nservice Laser {\n  // 监听Laser事件\n  rpc WatchEvent(google.protobuf.Empty) returns (stream LaserEventResp);\n}\n\n// 服务端下发Laser事件\nmessage LaserEventResp {\n  // 任务id\n  int64 taskid = 1;\n  // 指令名\n  string action = 2;\n  // 指令参数json字符串\n  string params = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/cheese/gateway/player/v1/playurl.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.cheese.gateway.player.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/playurl/v1/playurl.proto\";\n\n// 课程视频url\nservice PlayURL {\n  // 播放页信息\n  rpc PlayView (PlayViewReq) returns (PlayViewReply);\n  // 投屏地址\n  rpc Project (ProjectReq) returns (ProjectReply);\n}\n\n// 播放页信息-请求\nmessage PlayViewReq {\n  // 课程epid(与番剧不互通)\n  int64 ep_id = 1;\n  // 视频cid\n  int64 cid = 2;\n  // 清晰度\n  int64 qn = 3;\n  // 视频流版本\n  int32 fnver = 4;\n  // 视频流格式\n  int32 fnval = 5;\n  // 下载模式\n  // 0:播放 1:flv下载 2:dash下载\n  uint32 download = 6;\n  // 流url强制是用域名\n  // 0:允许使用ip 1:使用http 2:使用https\n  int32 force_host = 7;\n  // 是否4K\n  bool fourk = 8;\n  // 当前页spm\n  string spmid = 9;\n  // 上一页spm\n  string from_spmid = 10;\n  // 青少年模式\n  int32 teenagers_mode = 11;\n  // 视频编码\n  bilibili.app.playurl.v1.CodeType prefer_codec_type = 12;\n  // 是否强制请求预览视频\n  bool is_preview = 13;\n}\n\n// 投屏地址-请求\nmessage ProjectReq {\n  // 课程epid(与番剧不互通)\n  int64 ep_id = 1;\n  // 视频cid\n  int64 cid = 2;\n  // 清晰度\n  int64 qn = 3;\n  // 视频流版本\n  int32 fnver = 4;\n  // 视频流格式\n  int32 fnval = 5;\n  // 下载模式\n  // 0:播放 1:flv下载 2:dash下载\n  uint32 download = 6;\n  // 流url强制是用域名\n  // 0:允许使用ip 1:使用http 2:使用https\n  int32 force_host = 7;\n  // 是否4K\n  bool fourk = 8;\n  // 当前页spm\n  string spmid = 9;\n  // 上一页spm\n  string from_spmid = 10;\n  // 投屏协议\n  // 0:默认乐播 1:自建协议 2:云投屏\n  int32 protocol = 11;\n  // 投屏设备\n  // 0:默认其他 1:OTT设备\n  int32 device_type = 12;\n  // 是否flv格式\n  bool flv_proj = 13;\n}\n\n// 播放页信息-响应\nmessage PlayViewReply {\n  // 视频url信息\n  bilibili.app.playurl.v1.VideoInfo video_info = 1;\n  // 禁用功能配置\n  PlayAbilityConf play_conf = 2;\n}\n\n// 禁用功能配置\nmessage PlayAbilityConf {\n  bool background_play_disable = 1; // 后台播放\n  bool flip_disable = 2;            // 镜像反转\n  bool cast_disable = 3;            // 支持投屏\n  bool feedback_disable = 4;        // 反馈\n  bool subtitle_disable = 5;        // 字幕\n  bool playback_rate_disable = 6;   // 播放速度\n  bool time_up_disable = 7;         // 定时停止播放\n  bool playback_mode_disable = 8;   // 播放方式\n  bool scale_mode_disable = 9;      // 画面尺寸\n  bool like_disable = 10;           // 顶\n  bool dislike_disable = 11;        // 踩\n  bool coin_disable = 12;           // 投币\n  bool elec_disable = 13;           // 充电\n  bool share_disable = 14;          // 分享\n  bool screen_shot_disable = 15;    // 截图\n  bool lock_screen_disable = 16;    // 锁屏\n  bool recommend_disable = 17;      // 相关推荐\n  bool playback_speed_disable = 18; // 倍速\n  bool definition_disable = 19;     // 清晰度\n  bool selections_disable = 20;     // 选集\n  bool next_disable = 21;           // 下一集\n  bool edit_dm_disable = 22;        // 编辑弹幕\n  bool outer_dm_disable = 25;       // 外层面板弹幕设置\n  bool inner_dm_disable = 26;       // 三点内弹幕设置\n  bool small_window_disable = 27;   // 画中画\n}\n\n// 投屏地址-响应\nmessage ProjectReply {\n  bilibili.app.playurl.v1.PlayURLReply project = 1;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/community/service/dm/v1/dm.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.community.service.dm.v1;\n\noption java_multiple_files = true;\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}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/community/service/govern/v1/govern.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.community.service.govern.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n//\nservice Qoe {\n  //\n  rpc QoeReport (QoeReportReq) returns (google.protobuf.Empty);\n}\n\n//\nmessage QoeReportReq {\n  //\n  int64 id = 1;\n  //\n  int64 scene = 2;\n  //\n  int32 type = 3;\n  //\n  bool cancel = 4;\n  //\n  string business_type = 5;\n  //\n  int64 oid = 6;\n  //\n  QoeScoreResult score_result = 7;\n  //\n  string business_data = 8;\n}\n\n//\nmessage QoeScoreResult {\n  //\n  float score = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/dagw/component/avatar/common/common.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.dagw.component.avatar.common;\n\noption java_multiple_files = true;\n\n//\nmessage BasicRenderSpec {\n  //\n  double opacity = 1;\n}\n\n//\nmessage ColorConfig {\n  //\n  bool is_dark_mode_aware = 1;\n  //\n  ColorSpec day = 2;\n  //\n  ColorSpec night = 3;\n}\n\n//\nmessage ColorSpec {\n  //\n  string argb = 1;\n}\n\n//\nmessage LayerGeneralSpec {\n  //\n  PositionSpec pos_spec = 1;\n  //\n  SizeSpec size_spec = 2;\n  //\n  BasicRenderSpec render_spec = 3;\n}\n\n//\nmessage MaskProperty {\n  //\n  LayerGeneralSpec general_spec = 1;\n  //\n  ResourceSource mask_src = 2;\n}\n\n//\nmessage NativeDrawRes {\n  //\n  int32 draw_type = 1;\n  //\n  int32 fill_mode = 2;\n  //\n  ColorConfig color_config = 3;\n  //\n  double edge_weight = 4;\n}\n\n//\nmessage PositionSpec {\n  //\n  int32 coordinate_pos = 1;\n  //\n  double axis_x = 2;\n  //\n  double axis_y = 3;\n}\n\n//\nmessage RemoteRes {\n  //\n  string url = 1;\n  //\n  string bfs_style = 2;\n}\n\n//\nmessage ResourceSource {\n  //\n  enum LocalRes {\n    LOCAL_RES_INVALID = 0;\n    LOCAL_RES_ICON_VIP = 1;\n    LOCAL_RES_ICON_SMALL_VIP = 2;\n    LOCAL_RES_ICON_PERSONAL_VERIFY = 3;\n    LOCAL_RES_ICON_ENTERPRISE_VERIFY = 4;\n    LOCAL_RES_ICON_NFT_MAINLAND = 5;\n    LOCAL_RES_DEFAULT_AVATAR = 6;\n  }\n  //\n  int32 src_type = 1;\n  //\n  int32 placeholder = 2;\n  //\n  oneof res {\n    //\n    RemoteRes remote = 3;\n    //\n    LocalRes local = 4;\n    //\n    NativeDrawRes draw = 5;\n  }\n}\n\n//\nmessage SizeSpec {\n  //\n  double width = 1;\n  //\n  double height = 2;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/dagw/component/avatar/v1/avatar.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.dagw.component.avatar.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/dagw/component/avatar/common/common.proto\";\nimport \"bilibili/dagw/component/avatar/v1/plugin.proto\";\n\n//\nmessage AvatarItem {\n  //\n  bilibili.dagw.component.avatar.common.SizeSpec container_size = 1;\n  //\n  repeated LayerGroup layers = 2;\n  //\n  LayerGroup fallback_layers = 3;\n  //\n  int64 mid = 4;\n}\n\n//\nmessage BasicLayerResource {\n  //\n  int32 res_type = 1;\n  //\n  oneof payload {\n    //\n    ResImage res_image = 2;\n    //\n    ResAnimation res_animation = 3;\n    ///\n    ResNativeDraw res_native_draw = 4;\n  };\n}\n\n//\nmessage GeneralConfig {\n  //\n  map<string, string> web_css_style = 1;\n}\n\n//\nmessage Layer {\n  //\n  string layer_id = 1;\n  //\n  bool visible = 2;\n  //\n  bilibili.dagw.component.avatar.common.LayerGeneralSpec general_spec = 3;\n  //\n  LayerConfig layer_config = 4;\n  //\n  BasicLayerResource resource = 5;\n}\n\n//\nmessage LayerConfig {\n  //\n  map<string, LayerTagConfig> tags = 1;\n  //\n  bool is_critical = 2;\n  //\n  bool allow_over_paint = 3;\n  //\n  bilibili.dagw.component.avatar.common.MaskProperty layer_mask = 4;\n}\n\n//\nmessage LayerGroup {\n  //\n  string group_id = 1;\n  //\n  repeated Layer layers = 2;\n  //\n  bilibili.dagw.component.avatar.common.MaskProperty group_mask = 3;\n  //\n  bool is_critical_group = 4;\n}\n\n//\nmessage LayerTagConfig {\n  //\n  int32 config_type = 1;\n  //\n  oneof config {\n    //\n    GeneralConfig general_config = 2;\n    //\n    bilibili.dagw.component.avatar.v1.plugin.GyroConfig gyro_config = 3;\n    //\n    bilibili.dagw.component.avatar.v1.plugin.CommentDoubleClickConfig comment_doubleClick_config = 4;\n    //\n    bilibili.dagw.component.avatar.v1.plugin.LiveAnimeConfig live_anime_config = 5;\n  };\n}\n\n//\nmessage ResAnimation {\n  //\n  bilibili.dagw.component.avatar.common.ResourceSource webp_src = 1;\n}\n\n//\nmessage ResImage {\n  //\n  bilibili.dagw.component.avatar.common.ResourceSource image_src = 1;\n}\n\n//\nmessage ResNativeDraw {\n  //\n  bilibili.dagw.component.avatar.common.ResourceSource draw_src = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/dagw/component/avatar/v1/plugin.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.dagw.component.avatar.v1.plugin;\n\noption java_multiple_files = true;\n\nimport \"bilibili/dagw/component/avatar/common/common.proto\";\n\n//\nmessage CommentDoubleClickConfig {\n  //\n  Interaction interaction = 1;\n  //\n  double animation_scale = 2;\n}\n\n//\nmessage GyroConfig {\n  //\n  NFTImageV2 gyroscope = 1;\n}\n\n//\nmessage GyroscopeContentV2 {\n  //\n  string file_url = 1;\n  //\n  float scale = 2;\n  //\n  repeated PhysicalOrientationV2 physical_orientation = 3;\n}\n\n//\nmessage GyroscopeEntityV2 {\n  //\n  string display_type = 1;\n  //\n  repeated GyroscopeContentV2 contents = 2;\n}\n\n//\nmessage Interaction {\n  //\n  string nft_id = 1;\n  //\n  bool enabled = 2;\n  //\n  string itype = 3;\n  //\n  string metadata_url = 4;\n}\n\n//\nmessage LiveAnimeConfig {\n  //\n  bool is_live = 1;\n}\n\n//\nmessage LiveAnimeItem {\n  //\n  bilibili.dagw.component.avatar.common.ColorConfig color = 1;\n  //\n  double start_ratio = 2;\n  //\n  double end_ratio = 3;\n  //\n  double start_stroke = 4;\n  //\n  double start_opacity = 5;\n  //\n  int64 phase = 6;\n}\n\n//\nmessage NFTImageV2 {\n  //\n  repeated GyroscopeEntityV2 gyroscope = 1;\n}\n\n//\nmessage PhysicalOrientationAnimation {\n  //\n  string type = 1;\n  //\n  string bezier = 3;\n}\n\n//\nmessage PhysicalOrientationV2 {\n  //\n  string type = 1;\n  //\n  repeated PhysicalOrientationAnimation animations = 3;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/dynamic/common/dynamic.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.dynamic;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/dynamic/v2/dynamic.proto\";\n\n// at分组信息\nmessage AtGroup {\n  // 分组类型\n  AtGroupType group_type = 1;\n  // 分组名称\n  string group_name = 2;\n  // items\n  repeated AtItem items = 3;\n}\n\n// at分组类型\nenum AtGroupType {\n  AT_GROUP_TYPE_DEFAULT = 0; // 默认\n  AT_GROUP_TYPE_RECENT = 1; // 最近联系\n  AT_GROUP_TYPE_FOLLOW = 2; // 我的关注（互相关注 > 单向关注）\n  AT_GROUP_TYPE_FANS = 3; // 我的粉丝\n  AT_GROUP_TYPE_OTHERS = 4; // 其他\n}\n\n// at返回单条信息\nmessage AtItem {\n  // mid\n  int64 uid = 1;\n  // 昵称\n  string name = 2;\n  // 头像\n  string face = 3;\n  // 粉丝数\n  int32 fans = 4;\n  // 认证信息\n  int32 official_verify_type = 5;\n}\n\n// at列表-请求\nmessage AtListReq {\n  // mid\n  int64 uid = 1;\n}\n\n// at列表-响应\nmessage AtListRsp {\n  // 分组信息\n  repeated AtGroup groups = 1;\n}\n\n// at搜索-请求\nmessage AtSearchReq {\n  // mid\n  int64 uid = 1;\n  // 关键字\n  string keyword = 2;\n}\n\n//\nenum AttachCardType {\n  ATTACH_CARD_NONE = 0;   // 无\n  ATTACH_CARD_GOODS = 1;   // 商品卡\n  ATTACH_CARD_VOTE = 2;   // 投票卡\n  ATTACH_CARD_UGC = 3;   // ugc视频卡\n  ATTACH_CARD_ACTIVITY = 4;   // 帮推\n  ATTACH_CARD_OFFICIAL_ACTIVITY = 5;   // 官方活动\n  ATTACH_CARD_TOPIC = 6;   // 话题活动\n  ATTACH_CARD_OGV = 7;   // OGV\n  ATTACH_CARD_AUTO_OGV = 8;   // OGV自动出卡\n  ATTACH_CARD_GAME = 9;   // 游戏\n  ATTACH_CARD_MANGA = 10;  // 漫画\n  ATTACH_CARD_DECORATION = 11;  // 装扮\n  ATTACH_CARD_MATCH = 12;  // 赛事\n  ATTACH_CARD_PUGV = 13;  // 课程\n  ATTACH_CARD_RESERVE = 14;  // 预约\n  ATTACH_CARD_UP_TOPIC = 15;  // up主话题活动\n}\n\n//\nmessage BottomBusiness {\n  // 业务方资源id\n  int64 rid = 1;\n  // 业务方类型，定义在BottomBizType中\n  int64 type = 2;\n}\n\n//\nenum ContentType {\n  CONTENT_TYPE_NONE = 0;   // 占位\n  TEXT = 1;   // 文本，简单内容，biz_id就是文本\n  AT = 2;   // @用户，简单内容，biz_id是用户uid\n  LOTTERY = 3;   // 抽奖，简单内容，biz_id是抽奖id\n  VOTE = 4;   // 投票，简单内容，biz_id是投票id\n  TOPIC = 5;   // 话题，简单内容，biz_id是话题id\n  GOODS = 6;   // 商品文字链，复杂内容，定义在GoodsContent结构，biz_id为空\n  BV = 7;   // bv，简单内容，biz_id是bvid，包括\"BV1\"等内容\n  AV = 8;   // av，简单内容，biz_id是avid\n  EMOJI = 9;   // 表情，简单内容，biz_id为空\n  USER = 10;  // 外露用户，暂未使用\n  CV = 11;  // 专栏，简单内容，biz_id是cvid\n  VC = 12;  // 废弃业务，无用\n  WEB = 13;  // 网址，简单内容，biz_id是网页链接\n  TAOBAO = 14;  // 淘宝内容，暂时不用\n  MAIL = 15;  // 邮箱，简单内容，biz_id是邮箱地址\n  OGV_SEASON = 16;  // 番剧season，简单内容，biz_id是番剧的season_id\n  OGV_EP = 17;  // 番剧ep，简单内容，biz_id是番剧的epid\n}\n\n//\nmessage CreateActivity {\n  //\n  int64 activity_id = 1;\n  //\n  int32 activity_state = 2;\n  //\n  int32 is_new_activity = 3;\n  //\n  int32 action = 4;\n}\n\n// 动态附带的附加大卡\nmessage CreateAttachCard {\n  // 商品大卡\n  CreateGoodsCard goods = 1;\n  // 通用附加大卡，目前仅限定Match,Game,Ugc,Pugv,Reserve，且同时只能有一个\n  CreateCommonAttachCard common_card = 2;\n}\n\n// 发布页预校验-响应\nmessage CreateCheckResp {\n  // 发布相关的配置项\n  PublishSetting setting = 1;\n  // 用户具有的发布权限\n  UpPermission permission = 2;\n  // 分享渠道信息\n  ShareChannel share_info = 3;\n  // 小黄条\n  PublishYellowBar yellow_bar = 4;\n  //\n  PlusRedDot plus_red_dot = 5;\n}\n\n// 创建动态时附带的通用附加卡详情\nmessage CreateCommonAttachCard {\n  // 通用附加卡的类型\n  AttachCardType type = 1;\n  // 通用附加卡的业务id\n  int64 biz_id = 2;\n  //\n  int32 reserve_source = 3;\n  //\n  int32 reserve_lottery = 4;\n}\n\n// 动态-描述文字模块\nmessage CreateContent {\n  // 描述信息（已按高亮拆分）\n  repeated CreateContentItem contents = 1;\n}\n\n// 文本描述\nmessage CreateContentItem {\n  // 原始文案\n  string raw_text = 1;\n  // 类型\n  ContentType type = 2;\n  // 简单内容，可能为文字，BVID，AVID，uid等；复杂内容需要单独定义结构体\n  string biz_id = 3;\n  // 商品内容\n  GoodsContent goods = 4;\n}\n\n//\nmessage CreateDynVideo {\n  // 投稿平台来源，具体写什么@产品\n  string relation_from = 1;\n  // 1 — 投稿入口 + 相册选择视频 2 — 投稿入口 + 拍摄 3 — 小视频入口 + 相册选择视频 4 — 小视频入口 + 拍摄\n  int32 biz_from = 3;\n  // 投稿类型:  2-转载、1-自制\n  int32 copyright = 4;\n  // 是否公开投稿 0允许公开，1不允许公开 默认 0公开\n  int32 no_public = 5;\n  // 是否允许转载字段 0允许，1不允许，默认为0    copyright = 1 自制的时候默认勾选上no_reprint=1\n  int32 no_reprint = 6;\n  // 转载的时候必须填写，非空字符串\n  string source = 7;\n  // 稿件封面必须填写,不能为空 封面不支持其他源站链接 请确保 cover 是 先经过上传接口\n  string cover = 8;\n  // 稿件标题\n  string title = 9;\n  // 稿件分区ID 必须是有效的二级分区ID\n  int64 tid = 10;\n  // 标签 多个标签请使用英文逗号连接\n  string tag = 11;\n  // 稿件描述\n  string desc = 12;\n  // 当前输入环境下有，就输入http://domain/x/app/archive/desc/format返回的desc_format值\n  // 如果返回null就输入默认为0， 表示当前环境（分区+投稿类型）不参与简介格式化\n  int64 desc_format_id = 13;\n  // 稿件是否开启充电面板，1为是, 0为否\n  int32 open_elec = 14;\n  // 定时发布的时间\n  int32 dtime = 15;\n  // 分P聚合字段\n  repeated DynVideoMultiP videos = 16;\n  // 水印信息\n  DynVideoWatermark watermark = 17;\n  // 新增加通过tag来参加活动\n  int64 mission_id = 18;\n  // 新增加可以添加动态内容\n  string dynamic = 19;\n  // 序列化后的extend_info扩展信息\n  string dynamic_extension = 20;\n  // 客户端控制字段\n  string dynamic_ctrl = 21;\n  // 动态来源\n  string dynamic_from = 22;\n  // 抽奖服务生成的ID\n  int64 lottery_id = 23;\n  //\n  DynVideoVote vote = 24;\n  // 精选评论开关, true为开\n  bool up_selection_reply = 25;\n  // up主关闭评论\n  bool up_close_reply = 26;\n  // up主关闭弹幕\n  bool up_close_danmu = 27;\n  // 稿件投稿来源\n  int64 up_from = 28;\n  //\n  int64 duration = 29;\n}\n\n// 创建动态视频的应答包（透传给客户端）\nmessage CreateDynVideoResult {\n  // 稿件id\n  int64 aid = 1;\n  // 说明信息\n  string message = 2;\n  // 推荐的活动信息\n  DynVideoSubmitActBanner submitact_banner = 3;\n  //\n  DynVideoPushIntro push_intro = 4;\n}\n\n// 创建动态时附带的商品大卡详情\nmessage CreateGoodsCard {\n  // 商品大卡中的商品id\n  repeated string item_id = 1;\n}\n\n// 发布页预校验场景\nenum CreateInitCheckScene {\n  CREATE_INIT_CHECK_SCENE_INVALID = 0; //\n  CREATE_INIT_CHECK_SCENE_NORMAL = 1; // 动态页面右上角点击进入发布页\n  CREATE_INIT_CHECK_SCENE_REPOST = 2; // 动态feed流转发、三点分享，动态详情页转发\n  CREATE_INIT_CHECK_SCENE_SHARE = 3; // 其他页面分享到动态\n  CREATE_INIT_CHECK_SCENE_RESERVE_SHARE = 4; //\n}\n\n// 动态创建时的特殊选项\nmessage CreateOption {\n  // 评论区展示UP自己精选的评论\n  int32 up_choose_comment = 1;\n  // 初始评论区是关闭状态\n  int32 close_comment = 2;\n  // 该动态不会被折叠\n  // 目前仅抽奖开奖动态不会被折叠\n  int32 fold_exclude = 3;\n  // 审核等级，仅服务端发布时有效\n  // 100：自动过审\n  // 非100：默认的内网审核\n  // 默认为0\n  int32 audit_level = 4;\n  // 根据转发内容同步生成一条源动态/资源的评论\n  // 仅转发和分享时有效\n  int32 sync_to_comment = 5;\n  //\n  VideoShareInfo video_share_info = 6;\n  //\n  CreateActivity activity = 7;\n}\n\n// 创建图文动态时的图片信息\nmessage CreatePic {\n  // 上传图片URL\n  string img_src = 1;\n  // 图片宽度\n  double img_width = 2;\n  // 图片高度\n  double img_height = 3;\n  // 图片大小，单位KB\n  double img_size = 4;\n  //\n  repeated CreatePicTag img_tags = 5;\n}\n\n//\nmessage CreatePicTag {\n  //\n  int64 item_id = 1;\n  //\n  int64 tid = 2;\n  //\n  int64 mid = 3;\n  //\n  string text = 4;\n  //\n  string text_string = 5;\n  //\n  int64 type = 6;\n  //\n  int64 source_type = 7;\n  //\n  string url = 8;\n  //\n  string schema_url = 9;\n  //\n  string jump_url = 10;\n  //\n  int64 orientation = 11;\n  //\n  int64 x = 12;\n  //\n  int64 y = 13;\n  //\n  string poi = 14;\n}\n\n// 创建动态-响应\nmessage CreateResp {\n  // 动态id\n  int64 dyn_id = 1;\n  // 动态id str\n  string dyn_id_str = 2;\n  // 动态的类型\n  int64 dyn_type = 3;\n  // 动态id\n  int64 dyn_rid = 4;\n  // 假卡\n  bilibili.app.dynamic.v2.DynamicItem fake_card = 5;\n  // 视频\n  CreateDynVideoResult video_result = 6;\n}\n\n// 发布类型（场景）\nenum CreateScene {\n  CREATE_SCENE_INVALID = 0;  //\n  CREATE_SCENE_CREATE_WORD = 1;  // 发布纯文字动态\n  CREATE_SCENE_CREATE_DRAW = 2;  // 发布图文动态\n  CREATE_SCENE_CREATE_DYN_VIDEO = 3;  // 发布动态视频\n  CREATE_SCENE_REPOST = 4;  // 转发动态\n  CREATE_SCENE_SHARE_BIZ = 5;  // 分享业务方资源\n  CREATE_SCENE_SHARE_PAGE = 6;  // 分享网页（通用模板）\n  CREATE_SCENE_SHARE_PROGRAM = 7;  // 分享小程序\n  CREATE_SCENE_REPLY_SYNC = 8;  // 评论同步到动态\n  CREATE_SCENE_REPLY_CREATE_ACTIVITY = 9;  // 评论同步到动态并且发起活动\n}\n\n// 动态附带的小卡\nmessage CreateTag {\n  // lbs小卡\n  ExtLbs lbs = 1;\n  // 游戏通过SDK发布的动态需要带上游戏小卡\n  BottomBusiness sdk_game = 2;\n  // 必剪发布的动态需要带上必剪小卡\n  BottomBusiness diversion = 3;\n}\n\n//\nmessage CreateTopic {\n  //\n  int64 id = 1;\n  //\n  string name = 2;\n}\n\n// 动态的标识\nmessage DynIdentity {\n  // 动态id\n  int64 dyn_id = 1;\n  // 动态反向id，通过(type+rid组合)也可以唯一标识一个动态，与dyn_id出现任意一个即可\n  DynRevsId revs_id = 2;\n}\n\n//\nmessage DynRevsId {\n  // 动态类型\n  int64 dyn_type = 1;\n  // 业务id\n  int64 rid = 2;\n}\n\n// 动态视频分P视频编辑环境上报信息\nmessage DynVideoEditor {\n  //\n  int64 cid = 1;\n  //\n  int32 upfrom = 2;\n  // 滤镜\n  string filters = 3;\n  // 字体\n  string fonts = 4;\n  // 字幕\n  string subtitles = 5;\n  // bgm\n  string bgms = 6;\n  // 3d拍摄贴纸\n  string stickers = 7;\n  // 2d投稿贴纸\n  string videoup_stickers = 8;\n  // 视频转场特效\n  string trans = 9;\n  // 编辑器的主题使用相关\n  string makeups = 10;\n  // 整容之外科手术\n  string surgerys = 11;\n  // 美摄特定的videofx\n  string videofxs = 12;\n  // 编辑器的主题使用相关\n  string themes = 13;\n  // 拍摄之稿件合拍\n  string cooperates = 14;\n  // 拍摄之音乐卡点视频\n  string rhythms = 15;\n  // mvp特效\n  string effects = 16;\n  // mvp背景\n  string backgrounds = 17;\n  // mvp视频\n  string videos = 18;\n  // mvp音效\n  string sounds = 19;\n  // mvp花字\n  string flowers = 20;\n  // mvp封面模板\n  string cover_templates = 21;\n  // tts\n  string tts = 22;\n  // openings\n  string openings = 23;\n  // 录音题词\n  bool record_text = 24;\n  // 虚拟形象上报\n  string vupers = 25;\n  //\n  string features = 26;\n  //\n  string bcut_features = 27;\n  //\n  int32 audio_record = 28;\n  //\n  int32 camera = 29;\n  //\n  int32 speed = 30;\n  //\n  int32 camera_rotate = 31;\n  //\n  int32 screen_record = 32;\n  //\n  int32 default_end = 33;\n  //\n  int32 duration = 34;\n  //\n  int32 pic_count = 35;\n  //\n  int32 video_count = 36;\n  //\n  int32 shot_duration = 37;\n  //\n  string shot_game = 38;\n  //\n  bool highlight = 39;\n  //\n  int32 highlight_cnt = 40;\n  //\n  int32 pip_count = 41;\n}\n\n//\nmessage DynVideoHotAct {\n  //\n  int64 act_id = 1;\n  //\n  int64 etime = 2;\n  //\n  int64 id = 3;\n  //\n  string pic = 4;\n  //\n  int64 stime = 5;\n  //\n  string title = 6;\n  //\n  string link = 7;\n}\n\n// 动态视频分P聚合字段\nmessage DynVideoMultiP {\n  // 分P标题\n  string title = 1;\n  // 分P的文件名\n  string filename = 2;\n  //\n  int64 cid = 3;\n  // 编辑环境上报信息\n  DynVideoEditor editor = 4;\n}\n\n//\nmessage DynVideoPushIntro {\n  //\n  int32 show = 1;\n  //\n  string text = 2;\n}\n\n//\nmessage DynVideoSubmitActBanner {\n  //\n  string hotact_text = 1;\n  //\n  string hotact_url = 2;\n  //\n  repeated DynVideoHotAct list = 3;\n}\n\n//\nmessage DynVideoVote {\n  //\n  int64 vote_id = 1;\n  //\n  string vote_title = 2;\n  //\n  int32 top_for_reply = 3;\n}\n\n// 动态视频水印信息\nmessage DynVideoWatermark {\n  // 水印状态\n  // 0-关闭 1-打开 2-预览\n  int32 state = 1;\n  // 类型\n  // 1-用户昵称类型 2-用户id类型 3-用户名在logo下面\n  int32 type = 2;\n  // 位置\n  // 1-左上 2-右上 3-左下 4-右下\n  int32 position = 3;\n}\n\n//\nmessage ExtLbs {\n  //\n  string address = 1;\n  //\n  int64 distance = 2;\n  //\n  int64 type = 3;\n  //\n  string poi = 4;\n  //\n  LbsLoc location = 5;\n  //\n  string show_title = 6;\n  //\n  string title = 7;\n  //\n  string show_distance = 8;\n}\n\n// 根据name取uid-请求\nmessage GetUidByNameReq {\n  // 查询昵称列表\n  repeated string names = 1;\n}\n\n// 根据name取uid-响应\nmessage GetUidByNameRsp {\n  // k:昵称 v:mid\n  map<string, int64> uids = 1;\n}\n\n// 发布时附带的商品卡的详细内容\nmessage GoodsContent {\n  // 商品类型\n  // 1淘宝、2会员购\n  int32 source_type = 1;\n  // 商品的id\n  int64 item_id = 2;\n  // 店铺的id，兼容老版本\n  int64 shop_id = 3;\n}\n\n// UP已经创建的活动列表\nmessage LaunchedActivity {\n  // 模块名称，示例：\"已创建的活动\"\n  string module_title = 1;\n  // 已创建的活动列表\n  repeated LaunchedActivityItem activities = 2;\n  // 展示更多按钮\n  // 已创建活动大于5个时下发\n  ShowMoreLaunchedActivity show_more = 3;\n}\n\n// UP已经创建的活动详情\nmessage LaunchedActivityItem {\n  // 活动id\n  int64 activity_id = 1;\n  // 活动名称\n  string activity_name = 2;\n  // 活动是否已上线\n  // 0未上线 1已上线\n  int32 activity_state = 3;\n}\n\n//\nmessage LbsLoc {\n  // 经度\n  double lat = 1;\n  // 纬度\n  double lng = 2;\n}\n\n//\nmessage MetaDataCtrl {\n  // 客户端平台\n  string platform = 1;\n  // 客户端build号\n  string build = 2;\n  // 客户端移动设备类型\n  string mobi_app = 3;\n  // 客户端buvid\n  string buvid = 4;\n  // 用户设备信息\n  string device = 5;\n  // 请求来源页面的spmid\n  string from_spmid = 6;\n  // 请求来源页面\n  string from = 7;\n  // 请求的trace_id\n  string trace_id = 8;\n  // 青少年模式\n  int32 teenager_mode = 9;\n  // 0:正常 1:冷启动\n  int32 cold_start = 10;\n  // 客户端版本号\n  string version = 11;\n  // 网络状态\n  // Unknown=0 WIFI=1 WWAN=2\n  int32 network = 12;\n  // 用户ip地址\n  string ip = 13;\n}\n\n//\nmessage PlusRedDot {\n  //\n  int64 plus_has_red_dot = 1;\n}\n\n// 小程序内容定义\nmessage Program {\n  // 标题\n  string title = 1;\n  // 描述文字\n  string desc = 2;\n  // 封面图\n  string cover = 3;\n  // 跳转链接\n  string target_url = 4;\n  // 小程序icon\n  string icon = 5;\n  // 小程序名称\n  string program_text = 6;\n  // 跳转链接文案，如：去看看\n  string jump_text = 7;\n}\n\n// 发布相关的设置项\nmessage PublishSetting {\n  // 提示转为专栏的最小字数，使用utf-16编码计算字符数\n  int32 min_words_to_article = 1;\n  // 提示转为专栏的最大字数，使用utf-16编码计算字符数\n  int32 max_words_to_article = 2;\n  // gif上传的最大值，单位：MB\n  int32 upload_size = 3;\n}\n\n// 发布页小黄条\nmessage PublishYellowBar {\n  // 展示文案\n  string text = 1;\n  // 跳转链接\n  string url = 2;\n  // 展示图标\n  string icon = 3;\n}\n\n//\nmessage RepostInitCheck {\n  //\n  DynIdentity repost_src = 1;\n  //\n  string share_id = 2;\n  //\n  int32 share_mode = 3;\n}\n\n//\nenum ReserveSource {\n  RESERVE_SOURCE_NEW = 0; //\n  RESERVE_SOURCE_ASSOCIATED = 1; //\n}\n\n// 分享渠道信息\nmessage ShareChannel {\n  // 业务类型，如动态是\"dynamic\"\n  string share_origin = 1;\n  // 业务资源id\n  string oid = 2;\n  // 辅助id, 非必返回字段\n  string sid = 3;\n  // 渠道列表\n  repeated ShareChannelItem share_channels = 4;\n}\n\n// 渠道\nmessage ShareChannelItem {\n  // 展示文案\n  string name = 1;\n  // 展示图标\n  string picture = 2;\n  // 渠道名称\n  string share_channel = 3;\n  // 预约卡分享图信息，仅分享有预约信息的动态时存在\n  ShareReserve reserve = 4;\n}\n\n//\nmessage ShareReserve {\n  // 标题\n  string title = 1;\n  // 描述（时间+类型）\n  string desc = 2;\n  // 二维码附带icon\n  string qr_code_icon = 3;\n  // 二维码附带文本\n  string qr_code_text = 4;\n  // 二维码链接\n  string qr_code_url = 5;\n  //\n  string name = 6;\n  //\n  string face = 7;\n  //\n  ShareReservePoster poster = 8;\n  //\n  ShareReserveLottery reserve_lottery = 9;\n}\n\n//\nmessage ShareReserveLottery {\n  //\n  string icon = 1;\n  //\n  string text = 2;\n}\n\n//\nmessage ShareReservePoster {\n  //\n  string url = 1;\n  //\n  double width = 2;\n  //\n  double height = 3;\n}\n\n//\nmessage ShareResult {\n  //\n  int64 share_enable = 1;\n  //\n  string toast = 2;\n}\n\n// UP已经创建的活动列表中的展示更多按钮详情\nmessage ShowMoreLaunchedActivity {\n  // 按钮的文案\n  string button_text = 1;\n  // 按钮的跳转链接\n  string jump_url = 2;\n}\n\n// 通用模板的网页元内容(sketch结构)定义\nmessage Sketch {\n  // 元内容标题，长度30限制\n  string title = 1;\n  // 描述文字（文本内容第二行），长度233限制\n  string desc_text = 2;\n  // 文本文字（文本内容第三行），仅限竖图通用卡片使用，长度233限制\n  string text = 3;\n  // 表示业务方的id表示，对于在业务方有唯一标示的必填\n  int64 biz_id = 4;\n  // 业务类型，与展示时的右上角标有关，需要业务方向动态申请\n  int64 biz_type = 5;\n  // 封面图片链接地址，域名需要符合白名单\n  string cover_url = 6;\n  // 跳转链接地址，域名需要符合白名单\n  string target_url = 7;\n}\n\n// 发布相关的权限内容\nmessage UpPermission {\n  // 通用权限列表\n  repeated UpPermissionItem items = 1;\n  // 已经创建的活动列表\n  LaunchedActivity launched_activity = 2;\n  //\n  ShareResult share_result = 3;\n}\n\n// 通用发布权限内容的详细定义\nmessage UpPermissionItem {\n  // 类型，enum UpPermissionType\n  int32 type = 1;\n  // UP是否有权限\n  // 1-有，2-限制（展示但不可点，仅预约使用）\n  int32 permission = 2;\n  // 按钮文案\n  string title = 3;\n  // 功能开关的副标题\n  string subtitle = 4;\n  // 按钮图标的url地址\n  string icon = 5;\n  // 跳转链接，permission=1时点击按钮跳到此链接\n  string jump_url = 6;\n  // 错误提示，permission=2时点击按钮会弹出此提示，目前仅预约使用\n  string toast = 7;\n  //\n  int64 has_red_dot = 8;\n}\n\n//\nenum UpPermissionType {\n  UP_PERMISSION_TYPE_NONE = 0;  // 占位\n  UP_PERMISSION_TYPE_LOTTERY = 1;  // 是否是抽奖的灰度用户，默认不是\n  UP_PERMISSION_TYPE_CLIP_PUBLISHED = 2;  // 之前是否发过小视频，默认没发过\n  UP_PERMISSION_TYPE_UGC_ATTACH_CARD = 3;  // 是否可以添加ugc附加卡，默认不可以\n  UP_PERMISSION_TYPE_GOODS_ATTACH_CARD = 4;  // 是否有权限添加商品附加卡\n  UP_PERMISSION_TYPE_CHOOSE_COMMENT = 5;  // 是否有权限自主精选评论白名单，默认没有\n  UP_PERMISSION_TYPE_CONTROL_COMMENT = 6;  // 是否有权限关闭评论区，默认有\n  UP_PERMISSION_TYPE_CONTROL_DANMU = 7;  // 是否有权限关闭弹幕（仅对动态视频生效），默认有\n  UP_PERMISSION_TYPE_VIDEO_RESERVE = 8;  // 是否可以发起稿件预约\n  UP_PERMISSION_TYPE_LIVE_RESERVE = 9;  // 是否可以发起直播预约\n}\n\n// 用户主动发布（app/web发布）时的meta信息\nmessage UserCreateMeta {\n  // 用户发布客户端的meta信息\n  MetaDataCtrl app_meta = 1;\n  // 用户发布时的位置信息（经纬度）\n  LbsLoc loc = 2;\n  // 1-发布页转发 2-立即转发\n  int32 repost_mode = 3;\n}\n\n//\nmessage VideoShareInfo {\n  //\n  int64 cid = 1;\n  //\n  int32 part = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/dynamic/gw/gateway.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.dynamic.gateway;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/any.proto\";\n\n// 综合页请求广告所需字段，由客户端-网关透传\nmessage AdParam {\n  // 综合页请求广告所需字段，由客户端-网关透传\n  string ad_extra = 1;\n  // request_id\n  string request_id = 2;\n}\n\n//\nenum AddButtonBgStyle {\n  fill = 0;   // 默认填充\n  stroke = 1; // 描边\n  gray = 2;   // 置灰\n}\n\n// 按钮类型\nenum AddButtonType {\n  bt_none = 0;   // 占位\n  bt_jump = 1;   // 跳转\n  bt_button = 2; // 按钮\n}\n\n// 动态-附加卡-通用卡\nmessage AdditionCommon {\n  // 头部说明文案\n  string head_text = 1;\n  // 标题\n  string title = 2;\n  // 展示图\n  string image_url = 3;\n  // 描述文字1\n  string desc_text_1 = 4;\n  // 描述文字2\n  string desc_text_2 = 5;\n  // 点击跳转链接\n  string url = 6;\n  // 按钮\n  AdditionalButton button = 7;\n  // 头部icon\n  string head_icon = 8;\n  // style\n  ImageStyle style = 9;\n  // 动态本身的类型 type\n  string type = 10;\n  // 附加卡类型\n  string card_type = 11; // ogv manga\n}\n\n// 动态-附加卡-电竞卡\nmessage AdditionEsport {\n  // 电竞类型\n  EspaceStyle style = 1;\n  oneof item {\n    // moba类\n    AdditionEsportMoba addition_esport_moba = 2;\n  }\n  // 动态本身的类型 type\n  string type = 3;\n  // 附加卡类型\n  string card_type = 4; // ogv manga\n}\n\n// 动态-附加卡-电竞卡-moba类\nmessage AdditionEsportMoba {\n  // 头部说明文案\n  string head_text = 1;\n  // 标题\n  string title = 2;\n  // 战队列表\n  repeated MatchTeam match_team = 3;\n  // 比赛信息\n  AdditionEsportMobaStatus addition_esport_moba_status = 4;\n  // 卡片跳转\n  string uri = 5;\n  // 按钮\n  AdditionalButton button = 6;\n  // 副标题\n  string sub_title = 7;\n  // 动态本身的类型 type\n  string type = 10;\n  // 附加卡类型\n  string card_type = 11;\n}\n\n// 动态-附加卡-电竞卡-moba类-比赛信息\nmessage AdditionEsportMobaStatus {\n  // 文案类\n  repeated AdditionEsportMobaStatusDesc addition_esport_moba_status_desc = 1;\n  // 比赛状态文案\n  string title = 2;\n  // 比赛状态状态\n  int32 status = 3;\n  // 日间色值\n  string color = 4;\n  // 夜间色值\n  string night_color = 5;\n}\n\n// 动态-附加卡-电竞卡-moba类-比赛信息-文案类\nmessage AdditionEsportMobaStatusDesc {\n  // 文案\n  string title = 1;\n  // 日间色值\n  string color = 2;\n  // 夜间色值\n  string night_color = 3;\n}\n\n// 动态-附加卡-商品卡\nmessage AdditionGoods {\n  // 推荐文案\n  string rcmd_desc = 1;\n  // 商品信息\n  repeated GoodsItem goods_items = 2;\n  // 附加卡类型\n  string card_type = 3;\n  // 头部icon\n  string icon = 4;\n  // 商品附加卡整卡跳转\n  string uri = 5;\n  // 商品类型\n  // 1:淘宝 2:会员购，注：实际是获取的goods_items里面的第一个source_type\n  int32 source_type = 6;\n}\n\n// up主预约发布卡\nmessage AdditionUP {\n  // 标题\n  string title = 1;\n  // 高亮文本，描述文字1\n  HighlightText desc_text_1 = 2;\n  // 描述文字2\n  string desc_text_2 = 3;\n  // 点击跳转链接\n  string url = 4;\n  // 按钮\n  AdditionalButton button = 5;\n  // 附加卡类型\n  string card_type = 6;\n  // 预约人数(用于预约人数变化)\n  int64 reserve_total = 7;\n  // 活动皮肤\n  AdditionalActSkin act_skin = 8;\n}\n\n// 动态-附加卡-UGC视频附加卡\nmessage AdditionUgc {\n  // 说明文案\n  string head_text = 1;\n  // 稿件标题\n  string title = 2;\n  // 封面\n  string cover = 3;\n  // 描述文字1\n  string desc_text_1 = 4;\n  // 描述文字2\n  string desc_text_2 = 5;\n  // 接秒开\n  string uri = 6;\n  // 时长\n  string duration = 7;\n  // 标题支持换行-标题支持单行和双行，本期不支持填充up昵称，支持双行展示，字段默认为true\n  bool line_feed = 8;\n  // 附加卡类型\n  string card_type = 9;\n}\n\n// 动态-附加卡-投票\nmessage AdditionVote {\n  // 封面图\n  string image_url = 1;\n  // 标题\n  string title = 2;\n  // 展示项1\n  string text_1 = 3;\n  // button文案\n  string button_text = 4;\n  // 点击跳转链接\n  string url = 5;\n}\n\n// 动态模块-投票\nmessage AdditionVote2 {\n  // 投票类型\n  AdditionVoteType addition_vote_type = 1;\n  // 投票ID\n  int64 vote_id = 2;\n  // 标题\n  string title = 3;\n  // 已过期： xxx人参与· 投票已过期。button 展示去查看\n  // 未过期： xxx人参与· 剩xx天xx时xx分。button展示去投票\n  string label = 4;\n  // 剩余时间\n  int64 deadline = 5;\n  // 生效文案\n  string open_text = 6;\n  // 过期文案\n  string close_text = 7;\n  // 已投票\n  string voted_text = 8;\n  // 投票状态\n  AdditionVoteState state = 9;\n  // 投票信息\n  oneof item {\n    //\n    AdditionVoteWord addition_vote_word = 10;\n    //\n    AdditionVotePic addition_vote_pic = 11;\n    //\n    AdditionVoteDefaule addition_vote_defaule = 12;\n  }\n  // 业务类型\n  // 0:动态投票 1:话题h5组件\n  int32 biz_type = 13;\n  // 投票总人数\n  int64 total = 14;\n  // 附加卡类型\n  string card_type = 15;\n  // 异常提示\n  string tips = 16;\n  // 跳转地址\n  string uri = 17;\n  // 是否投票\n  bool is_voted = 18;\n  // 投票最多多选个数，单选为1\n  int32 choice_cnt = 19;\n  // 是否默认选中分享到动态\n  bool defaule_select_share = 20;\n}\n\n// 外露投票\nmessage AdditionVoteDefaule {\n  // 图片 多张\n  repeated string cover = 1;\n}\n\n// 外露图片类型\nmessage AdditionVotePic {\n  // 图片投票详情\n  repeated AdditionVotePicItem item = 1;\n}\n\n// 图片投票详情\nmessage AdditionVotePicItem {\n  // 选项索引，从1开始\n  int32 opt_idx = 1;\n  // 图片\n  string cover = 2;\n  // 选中状态\n  bool is_vote = 3;\n  // 人数\n  int32 total = 4;\n  // 占比\n  double persent = 5;\n  // 标题文案\n  string title = 6;\n  // 是否投票人数最多的选项\n  bool  is_max_option = 7;\n}\n\n// 投票状态\nenum AdditionVoteState {\n  addition_vote_state_none = 0;  //\n  addition_vote_state_open = 1;  //\n  addition_vote_state_close = 2; //\n}\n\n// 投票类型\nenum AdditionVoteType {\n  addition_vote_type_none = 0;    //\n  addition_vote_type_word = 1;    //\n  addition_vote_type_pic = 2;     //\n  addition_vote_type_default = 3; //\n}\n\n// 外露文字类型\nmessage AdditionVoteWord {\n  // 外露文字投票详情\n  repeated AdditionVoteWordItem item = 1;\n}\n\n// 外露文字投票详情\nmessage AdditionVoteWordItem {\n  // 选项索引，从1开始\n  int32 opt_idx = 1;\n  // 文案\n  string title = 2;\n  // 选中状态\n  bool is_vote = 3;\n  // 人数\n  int32 total = 4;\n  // 占比\n  double persent = 5;\n  // 是否投票人数最多的选项\n  bool  is_max_option = 6;\n}\n\n// 活动皮肤\nmessage AdditionalActSkin {\n  // 动画SVGA资源\n  string svga = 1;\n  // 动画SVGA最后一帧图片资源\n  string last_image = 2;\n  // 动画播放次数\n  int64 play_times = 3;\n}\n\n// 动态-附加卡-按钮\nmessage AdditionalButton {\n  // 按钮类型\n  AddButtonType type = 1;\n  // jump-跳转样式\n  AdditionalButtonStyle jump_style = 2;\n  // jump-跳转链接\n  string jump_url = 3;\n  // button-未点样式\n  AdditionalButtonStyle uncheck = 4;\n  // button-已点样式\n  AdditionalButtonStyle check = 5;\n  // button-当前状态\n  AdditionalButtonStatus status = 6;\n  // 按钮点击样式\n  AdditionalButtonClickType click_type = 7;\n}\n\n// 附加卡按钮点击类型\nenum AdditionalButtonClickType {\n  click_none = 0; // 通用按钮\n  click_up = 1;   // 预约卡按钮\n}\n\nmessage AdditionalButtonInteractive {\n  // 是否弹窗\n  string popups = 1;\n  // 弹窗确认文案\n  string confirm = 2;\n  // 弹窗取消文案\n  string cancel = 3;\n  //\n  string desc = 4;\n}\n\n//\nenum AdditionalButtonStatus {\n  none = 0;    //\n  uncheck = 1; //\n  check = 2;   //\n}\n\n// 动态-附加卡-按钮样式\nmessage AdditionalButtonStyle {\n  // icon\n  string icon = 1;\n  // 文案\n  string text = 2;\n  // 按钮点击交互\n  AdditionalButtonInteractive interactive = 3;\n  // 当前按钮填充样式\n  AddButtonBgStyle bg_style = 4;\n  // toast文案, 当disable=1时有效\n  string toast = 5;\n  // 当前按钮样式,\n  // 0:高亮 1:置灰(按钮不可点击)\n  DisableState disable = 6;\n}\n\n// 动态-附加卡-番剧卡\nmessage AdditionalPGC {\n  // 头部说明文案\n  string head_text = 1;\n  // 标题\n  string title = 2;\n  // 展示图\n  string image_url = 3;\n  // 描述文字1\n  string desc_text_1 = 4;\n  // 描述文字2\n  string desc_text_2 = 5;\n  // 点击跳转链接\n  string url = 6;\n  // 按钮\n  AdditionalButton button = 7;\n  // 头部icon\n  string head_icon = 8;\n  // style\n  ImageStyle style = 9;\n  // 动态本身的类型 type\n  string type = 10;\n}\n\n// 枚举-动态附加卡\nenum AdditionalType {\n  additional_none = 0;                // 占位\n  additional_type_pgc = 1;            // 附加卡-追番\n  additional_type_goods = 2;          // 附加卡-商品\n  additional_type_vote = 3;           // 附加卡投票\n  additional_type_common = 4;         // 附加通用卡\n  additional_type_esport = 5;         // 附加电竞卡\n  additional_type_up_rcmd = 6;        // 附加UP主推荐卡\n  additional_type_ugc = 7;            // 附加卡-ugc\n  additional_type_up_reservation = 8; // UP主预约卡\n}\n\n// 动态卡片列表\nmessage CardVideoDynList {\n  // 动态列表\n  repeated DynamicItem list = 1;\n  // 更新的动态数\n  int64 update_num = 2;\n  // 历史偏移\n  string history_offset = 3;\n  // 更新基础信息\n  string update_baseline = 4;\n  // 是否还有更多数据\n  bool has_more = 5;\n}\n\n// 视频页-我的追番\nmessage CardVideoFollowList {\n  // 查看全部(跳转链接)\n  string view_all_link = 1;\n  // 追番列表\n  repeated FollowListItem list = 2;\n}\n\n// 视频页-最近访问\nmessage CardVideoUpList {\n  // 标题展示文案\n  string title = 1;\n  // up主列表\n  repeated UpListItem list = 2;\n  // 服务端生成的透传上报字段\n  string footprint = 3;\n  // 直播数\n  int32 show_live_num = 4;\n  // 跳转label\n  UpListMoreLabel more_label = 5;\n  // 标题开关(综合页)\n  int32 title_switch = 6;\n  // 是否展示右上角查看更多label\n  bool show_more_label = 7;\n  // 是否在快速消费页查看更多按钮\n  bool show_in_personal = 8;\n  // 是否展示右侧查看更多按钮\n  bool show_more_button = 9;\n}\n\n// 评论外露展示项\nmessage CmtShowItem {\n  // 用户mid\n  int64 uid = 1;\n  // 用户昵称\n  string uname = 2;\n  // 点击跳转链接\n  string uri = 3;\n  // 评论内容\n  string comment = 4;\n}\n\n// 装扮卡片-粉丝勋章信息\nmessage DecoCardFan {\n  // 是否是粉丝\n  int32 is_fan = 1;\n  // 数量\n  int32 number = 2;\n  // 数量 str\n  string number_str = 3;\n  // 颜色\n  string color = 4;\n}\n\n// 装扮卡片\nmessage DecorateCard {\n  // 装扮卡片id\n  int64 id = 1;\n  // 装扮卡片链接\n  string card_url = 2;\n  // 装扮卡片点击跳转链接\n  string jump_url = 3;\n  // 粉丝样式\n  DecoCardFan fan = 4;\n}\n\n// 文本类型\nenum DescType {\n  desc_type_none = 0;         // 占位\n  desc_type_text = 1;         // 文本\n  desc_type_aite = 2;         // @\n  desc_type_lottery = 3;      // 抽奖\n  desc_type_vote = 4;         // 投票\n  desc_type_topic = 5;        // 话题\n  desc_type_goods = 6;        // 商品\n  desc_type_bv = 7;           // bv\n  desc_type_av = 8;           // av\n  desc_type_emoji = 9;        // 表情\n  desc_type_user = 10;        // 外露用户\n  desc_type_cv = 11;        // 专栏\n  desc_type_vc = 12;        // 小视频\n  desc_type_web = 13;        // 网址\n  desc_type_taobao = 14;     // 淘宝\n  desc_type_mail = 15;        // 邮箱\n  desc_type_ogv_season = 16;  // 番剧season\n  desc_type_ogv_ep = 17;      // 番剧ep\n}\n\n// 文本描述\nmessage Description {\n  // 文本内容\n  string text = 1;\n  // 文本类型\n  DescType type = 2;\n  // 点击跳转链接\n  string uri = 3;\n  // emoji类型\n  EmojiType emoji_type = 4;\n  // 商品类型\n  string goods_type = 5;\n  // 前置Icon\n  string icon_url = 6;\n  // icon_name\n  string icon_name = 7;\n  // 资源ID\n  string rid = 8;\n  // 商品卡特殊字段\n  ModuleDescGoods goods = 9;\n  // 文本原始文案\n  string orig_text = 10;\n}\n\n// 尺寸信息\nmessage Dimension {\n  //\n  int64 height = 1;\n  //\n  int64 width = 2;\n  //\n  int64 rotate = 3;\n}\n\n//\nenum DisableState {\n  highlight = 0; // 高亮\n  gary = 1;      // 置灰(按钮不可点击)\n}\n\n// 动态通用附加卡-follow/取消follow-响应\nmessage DynAdditionCommonFollowReply {\n  //\n  AdditionalButtonStatus status = 1;\n}\n\n// 动态通用附加卡-follow/取消follow-请求\nmessage DynAdditionCommonFollowReq {\n  //\n  AdditionalButtonStatus status = 1;\n  //\n  string dyn_id = 2;\n  //\n  string card_type = 3;\n}\n\n// 最近访问-个人feed流列表-返回\nmessage DynAllPersonalReply {\n  // 动态列表\n  repeated DynamicItem list = 1;\n  // 偏移量\n  string offset = 2;\n  // 是否还有更多数据\n  bool has_more = 3;\n  // 已读进度\n  string read_offset = 4;\n  // 关注状态\n  Relation relation = 5;\n}\n\n// 最近访问-个人feed流列表-请求\nmessage DynAllPersonalReq {\n  // 被访问者的 UID\n  int64 host_uid = 1;\n  // 偏移量 第一页可传空\n  string offset = 2;\n  // 标明下拉几次\n  int32 page = 3;\n  // 是否是预加载 默认是1；客户端预加载。1：是预加载，不更新已读进度，不会影响小红点；0：非预加载，更新已读进度\n  int32 is_preload = 4;\n  // 秒开参数 新版本废弃，统一使用player_args\n  PlayurlParam playurl_param = 5;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 6;\n  // 服务端生成的透传上报字段\n  string footprint = 7;\n  // 来源\n  string from = 8;\n  // 秒开用\n  PlayerArgs player_args = 9;\n}\n\n// 动态综合页-响应\nmessage DynAllReply {\n  // 卡片列表\n  DynamicList dynamic_list = 1;\n  // 顶部up list\n  CardVideoUpList up_list = 2;\n  // 话题广场\n  TopicList topic_list = 3;\n  // 无关注推荐\n  Unfollow unfollow = 4;\n}\n\n// 动态综合页-请求\nmessage DynAllReq {\n  // 透传 update_baseline\n  string update_baseline = 1;\n  // 透传 history_offset\n  string offset = 2;\n  // 向下翻页数\n  int32 page = 3;\n  // 刷新方式 1向上刷新 2向下翻页\n  Refresh refresh_type = 4;\n  // 秒开参数 新版本废弃，统一使用player_args\n  PlayurlParam playurl_param = 5;\n  // 综合页当前更新的最大值\n  string assist_baseline = 6;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 7;\n  // 推荐up主入参(new的时候传)\n  RcmdUPsParam rcmd_ups_param = 8;\n  // 广告参数\n  AdParam ad_param = 9;\n  // 是否冷启\n  int32 cold_start = 10;\n  // 来源\n  string from = 11;\n  // 秒开参数\n  PlayerArgs player_args = 12;\n}\n\n// 最近访问-标记已读-请求\nmessage DynAllUpdOffsetReq {\n  // 被访问者的UID\n  int64 host_uid = 1;\n  // 用户已读进度\n  string read_offset = 2;\n  // 服务端生成的透传上报字段\n  string footprint = 3;\n}\n\n// 动态详情页-响应\nmessage DynDetailReply {\n  // 动态详情\n  DynamicItem item = 1;\n}\n\n// 批量动态id获取动态详情-请求\nmessage DynDetailsReq {\n  // 动态id\n  string dynamic_ids = 1;\n  // 秒开参数 新版本废弃，统一使用player_args\n  PlayurlParam playurl_param = 2;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 3;\n  // 秒开参数\n  PlayerArgs player_args = 4;\n}\n\n// 动态小卡类型\nenum DynExtendType {\n  dyn_ext_type_none = 0;     // 占位\n  dyn_ext_type_topic = 1;    // 话题小卡\n  dyn_ext_type_lbs = 2;      // lbs小卡\n  dyn_ext_type_hot = 3;      // 热门小卡\n  dyn_ext_type_game = 4;     // 游戏小卡\n  dyn_ext_type_common = 5;   // 通用小卡\n  dyn_ext_type_biliCut = 6;  // 必剪小卡\n  dyn_ext_type_ogv = 7;      // ogv小卡\n  dyn_ext_type_auto_ogv = 8; // 自动附加ogv小卡\n}\n\n// 动态发布生成临时卡-响应\nmessage DynFakeCardReply {\n  // 动态卡片\n  DynamicItem item = 1;\n}\n\n// 动态发布生成临时卡-请求\nmessage DynFakeCardReq {\n  //卡片内容json string\n  string content = 1;\n}\n\n// 查看更多-列表-响应\nmessage DynMixUpListViewMoreReply {\n  //\n  repeated MixUpListItem items = 1;\n  //\n  string  search_default_text = 2;\n  // 排序类型列表\n  repeated SortType  sort_types = 3;\n  // 是否展示更多的排序策略\n  bool show_more_sort_types = 4;\n  // 默认排序策略\n  int32 default_sort_type = 5;\n}\n\n// 查看更多-请求\nmessage DynMixUpListViewMoreReq {\n  // 排序策略\n  // 1:推荐排序 2:最常访问 3:最近关注，其他值为默认排序\n  int32 sort_type = 1;\n}\n\n// 动态模块类型\nenum DynModuleType {\n  module_none = 0;               // 占位\n  module_author = 1;             // 发布人模块\n  module_dispute = 2;            // 争议小黄条\n  module_desc = 3;               // 描述文案\n  module_dynamic = 4;            // 动态卡片\n  module_forward = 5;            // 转发模块\n  module_likeUser = 6;           // 点赞用户(废弃)\n  module_extend = 7;             // 小卡模块\n  module_additional = 8;         // 附加卡\n  module_stat = 9;               // 计数信息\n  module_fold = 10;              // 折叠\n  module_comment = 11;           // 评论外露(废弃)\n  module_interaction = 12;       // 外露交互模块(点赞、评论)\n  module_author_forward = 13;    // 转发卡的发布人模块\n  module_ad = 14;                // 广告卡模块\n  module_banner = 15;            // 通栏模块\n  module_item_null = 16;         // 获取物料失败模块\n  module_share_info = 17;        // 分享组件\n  module_recommend = 18;         // 相关推荐模块\n  module_stat_forward = 19;      // 转发卡计数信息\n  module_top = 20;               // 顶部模块\n  module_bottom = 21;            // 底部模块\n}\n\n// 关注推荐up主换一换-响应\nmessage DynRcmdUpExchangeReply {\n  // 无关注推荐\n  Unfollow unfollow = 1;\n}\n\n// 关注推荐up主换一换-请求\nmessage DynRcmdUpExchangeReq {\n  // 登录用户id\n  int64 uid = 1;\n  // 上一次不感兴趣的ts，单位：秒；该字段透传给搜索\n  int64 dislikeTs = 2;\n  // 需要与服务端确认或参照客户端现有参数\n  string from = 3;\n}\n\n// 动态点赞-请求\nmessage DynThumbReq {\n  // 用户uid\n  int64 uid = 1;\n  // 动态id\n  string dyn_id = 2;\n  // 动态类型(透传extend中的dyn_type)\n  int64 dyn_type = 3;\n  // 业务方资源id\n  string rid = 4;\n  // 点赞类型\n  ThumbType type = 5;\n}\n\n//\nenum DynUriType {\n  dyn_uri_type_none = 0;   //\n  dyn_uri_type_direct = 1; // 直接跳转对应uri\n  dyn_uri_type_suffix = 2; // 作为后缀拼接\n}\n\n// 最近访问-个人feed流列表-响应\nmessage DynVideoPersonalReply {\n  // 动态列表\n  repeated DynamicItem list = 1;\n  // 偏移量\n  string offset = 2;\n  // 是否还有更多数据\n  bool has_more = 3;\n  // 已读进度\n  string read_offset = 4;\n  // 关注状态\n  Relation relation = 5;\n}\n\n// 最近访问-个人feed流列表-请求\nmessage DynVideoPersonalReq {\n  // 被访问者的 UID\n  int64 host_uid = 1;\n  // 偏移量 第一页可传空\n  string offset = 2;\n  // 标明下拉几次\n  int32 page = 3;\n  // 是否是预加载\n  int32 is_preload = 4;\n  // 秒开参数 新版本废弃，统一使用player_args\n  PlayurlParam playurl_param = 5;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 6;\n  // 服务端生成的透传上报字段\n  string footprint = 7;\n  // 来源\n  string from = 8;\n  // 秒开参数\n  PlayerArgs player_args = 9;\n}\n\n// 动态视频页-响应\nmessage DynVideoReply {\n  // 卡片列表\n  CardVideoDynList dynamic_list = 1;\n  // 动态卡片\n  CardVideoUpList video_up_list = 2;\n  // 视频页-我的追番\n  CardVideoFollowList video_follow_list = 3;\n}\n\n// 动态视频页-请求\nmessage DynVideoReq {\n  // 透传 update_baseline\n  string update_baseline = 1;\n  // 透传 history_offset\n  string offset = 2;\n  // 向下翻页数\n  int32 page = 3;\n  // 刷新方式\n  // 1:向上刷新 2:向下翻页\n  Refresh refresh_type = 4;\n  // 秒开参数 新版本废弃，统一使用player_args\n  PlayurlParam playurl_param = 5;\n  // 综合页当前更新的最大值\n  string assist_baseline = 6;\n  // 客户端时区 兼容UTC-14和Etc/GMT+12,时区区间[-12,14] 东八区为8\n  int32 local_time = 7;\n  // 来源\n  string from = 8;\n  // 秒开参数\n  PlayerArgs player_args = 9;\n}\n\n// 最近访问-标记已读-请求\nmessage DynVideoUpdOffsetReq {\n  // 被访问者的UID\n  int64 host_uid = 1;\n  // 用户已读进度\n  string read_offset = 2;\n  // 服务端生成的透传上报字段\n  string footprint = 3;\n}\n\n// 投票操作-响应\nmessage DynVoteReply {\n  // 投票详情\n  AdditionVote2 item = 1;\n  // 投票操作返回状态\n  string toast = 2;\n}\n\n// 投票操作-请求\nmessage DynVoteReq {\n  // 投票ID\n  int64 vote_id = 1;\n  // 选项索引数组\n  repeated int64 votes = 2;\n  // 状态\n  VoteStatus status = 3;\n  // 动态ID\n  string dynamic_id = 4;\n  // 是否分享\n  bool share = 5;\n}\n\n// 动态卡片\nmessage DynamicItem {\n  // 动态卡片类型\n  DynamicType card_type = 1;\n  // 转发类型下，源卡片类型\n  DynamicType item_type = 2;\n  // 模块内容\n  repeated Module modules = 3;\n  // 操作相关字段\n  Extend extend = 4;\n  // 该卡片下面是否含有折叠卡\n  int32 has_fold = 5;\n}\n\n//动态卡片列表\nmessage DynamicList {\n  // 动态列表\n  repeated DynamicItem list = 1;\n  // 更新的动态数\n  int64 update_num = 2;\n  // 历史偏移\n  string history_offset = 3;\n  // 更新基础信息\n  string update_baseline = 4;\n  // 是否还有更多数据\n  bool has_more = 5;\n}\n\n// 枚举-动态类型\nenum DynamicType {\n  dyn_none = 0;          // 占位\n  forward = 1;           // 转发\n  av = 2;                // 稿件: ugc、小视频、短视频、UGC转PGC\n  pgc = 3;               // pgc：番剧、PGC番剧、PGC电影、PGC电视剧、PGC国创、PGC纪录片\n  courses = 4;           // 付费更新批次\n  fold = 5;              // 折叠\n  word = 6;              // 纯文字\n  draw = 7;              // 图文\n  article = 8;           // 专栏 原仅phone端\n  music = 9;             // 音频 原仅phone端\n  common_square = 10;    // 通用卡 方形\n  common_vertical = 11;  // 通用卡 竖形\n  live = 12;             // 直播卡 只有转发态\n  medialist = 13;        // 播单 原仅phone端 只有转发态\n  courses_season = 14;   // 付费更新批次 只有转发态\n  ad = 15;               // 广告卡\n  applet = 16;           // 小程序卡\n  subscription = 17;     // 订阅卡\n  live_rcmd = 18;        // 直播推荐卡\n  banner = 19;           // 通栏\n  ugc_season = 20;       // 合集卡\n  subscription_new = 21; // 新订阅卡\n}\n\n// 表情包类型\nenum EmojiType {\n  emoji_none = 0; // 占位\n  emoji_old = 1;  // emoji旧类型\n  emoji_new = 2;  // emoji新类型\n  vip = 3;        // 大会员表情\n}\n\n// 附加大卡-电竞卡样式\nenum EspaceStyle {\n  moba = 0; // moba类\n}\n\n// 动态-拓展小卡模块-通用小卡\nmessage ExtInfoCommon {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n  // poiType\n  int32 poi_type = 4;\n  // 类型\n  DynExtendType type = 5;\n  // 客户端埋点用\n  string sub_module = 6;\n  // 行动点文案\n  string action_text = 7;\n  // 行动点链接\n  string action_url = 8;\n  // 资源rid\n  int64 rid = 9;\n  // 轻浏览是否展示\n  bool is_show_light = 10;\n}\n\n// 动态-拓展小卡模块-游戏小卡\nmessage ExtInfoGame {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n}\n\n// 动态-拓展小卡模块-热门小卡\nmessage ExtInfoHot {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n}\n\n// 动态-拓展小卡模块-lbs小卡\nmessage ExtInfoLBS {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n  // poiType\n  int32 poi_type = 4;\n}\n\n// 动态-拓展小卡模块-ogv小卡\nmessage ExtInfoOGV {\n  // ogv小卡\n  repeated InfoOGV info_ogv = 1;\n}\n\n// 动态-拓展小卡模块-话题小卡\nmessage ExtInfoTopic {\n  // 标题-话题名\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n}\n\n// 扩展字段，用于动态部分操作使用\nmessage Extend {\n  // 动态id\n  string dyn_id_str = 1;\n  // 业务方id\n  string business_id = 2;\n  // 源动态id\n  string orig_dyn_id_str = 3;\n  // 转发卡：用户名\n  string orig_name = 4;\n  // 转发卡：图片url\n  string orig_img_url = 5;\n  // 转发卡：文字内容\n  repeated Description orig_desc = 6;\n  // 填充文字内容\n  repeated Description desc = 7;\n  // 被转发的源动态类型\n  DynamicType orig_dyn_type = 8;\n  // 分享到站外展示类型\n  string share_type = 9;\n  // 分享的场景\n  string share_scene = 10;\n  // 是否快速转发\n  bool is_fast_share = 11;\n  // r_type 分享和转发\n  int32 r_type = 12;\n  // 数据源的动态类型\n  int64 dyn_type = 13;\n  // 用户id\n  int64 uid = 14;\n  // 卡片跳转\n  string card_url = 15;\n  // 透传字段\n  google.protobuf.Any source_content = 16;\n  // 转发卡：用户头像\n  string orig_face = 17;\n  // 评论跳转\n  ExtendReply reply = 18;\n}\n\n// 评论扩展\nmessage ExtendReply {\n  // 基础跳转地址\n  string uri = 1;\n  // 参数部分\n  repeated ExtendReplyParam params = 2;\n}\n\n// 评论扩展参数部分\nmessage ExtendReplyParam {\n  // 参数名\n  string key = 1;\n  // 参数值\n  string value = 2;\n}\n\n// 折叠类型\nenum FoldType {\n  FoldTypeZore = 0;     // 占位\n  FoldTypePublish = 1;  // 用户发布折叠\n  FoldTypeFrequent = 2; // 转发超频折叠\n  FoldTypeUnite = 3;    // 联合投稿折叠\n  FoldTypeLimit = 4;    // 动态受限折叠\n}\n\n// 视频页-我的追番-番剧信息\nmessage FollowListItem {\n  // season_id\n  int64 season_id = 1;\n  // 标题\n  string title = 2;\n  // 封面图\n  string cover = 3;\n  // 跳转链接\n  string url = 4;\n  // new_ep\n  NewEP new_ep = 5;\n  // 子标题\n  string sub_title = 6;\n  // 卡片位次\n  int64 pos = 7;\n}\n\n//\nenum FollowType {\n  ft_not_follow = 0; //\n  ft_follow = 1;     //\n}\n\n// 动态-附加卡-商品卡-商品\nmessage GoodsItem {\n  // 图片\n  string cover = 1;\n  // schemaPackageName(Android用)\n  string schema_package_name = 2;\n  // 商品类型\n  // 1:淘宝 2:会员购\n  int32 source_type = 3;\n  // 跳转链接\n  string jump_url = 4;\n  // 跳转文案\n  string jump_desc = 5;\n  // 标题\n  string title = 6;\n  // 摘要\n  string brief = 7;\n  // 价格\n  string price = 8;\n  // item_id\n  int64 item_id = 9;\n  // schema_url\n  string schema_url = 10;\n  // open_white_list\n  repeated string open_white_list = 11;\n  // use_web_v2\n  bool user_web_v2 = 12;\n  // ad mark\n  string ad_mark = 13;\n}\n\n// 高亮文本\nmessage HighlightText {\n  // 展示文本\n  string text = 1;\n  // 高亮类型\n  HighlightTextStyle text_style = 2;\n}\n\n// 文本高亮枚举\nenum HighlightTextStyle {\n  style_none = 0;      // 默认\n  style_highlight = 1; // 高亮\n}\n\n// 枚举-附加卡样式\nenum ImageStyle {\n  add_style_vertical = 0; //\n  add_style_square = 1;   //\n}\n\n// 动态-拓展小卡模块-ogv小卡-(one of 片单、榜单、分区)\nmessage InfoOGV {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 小图标\n  string icon = 3;\n  // 客户端埋点用\n  string sub_module = 4;\n}\n\n// 外露交互模块\nmessage InteractionItem {\n  // 外露模块类型\n  LocalIconType icon_type = 1;\n  // 外露模块文案\n  repeated Description desc = 2;\n  // 外露模块uri相关 根据type不同用法不同\n  string uri = 3;\n  // 动态id\n  string dynamic_id = 4;\n  // 评论mid\n  int64 comment_mid = 6;\n}\n\n// 点赞动画\nmessage LikeAnimation {\n  // 开始动画\n  string begin = 1;\n  // 过程动画\n  string proc = 2;\n  // 结束动画\n  string end = 3;\n  // id\n  int64 like_icon_id = 4;\n}\n\n// 点赞拓展信息\nmessage LikeInfo {\n  // 点赞动画\n  LikeAnimation animation = 1;\n  // 是否点赞\n  bool is_like = 2;\n}\n\n// 点赞用户\nmessage LikeUser {\n  // 用户mid\n  int64 uid = 1;\n  // 用户昵称\n  string uname = 2;\n  // 点击跳转链接\n  string uri = 3;\n}\n\n// 直播信息\nmessage LiveInfo {\n  // 是否在直播\n  // 0:未直播 1:正在直播 (废弃)\n  int32 is_living = 1;\n  // 跳转链接\n  string uri = 2;\n  // 直播状态\n  LiveState live_state = 3;\n}\n\n// 直播状态\nenum LiveState {\n  live_none = 0;     // 未直播\n  live_live = 1;     // 直播中\n  live_rotation = 2; // 轮播中\n}\n\n// 外露模块类型\nenum LocalIconType {\n  local_icon_comment = 0; //\n  local_icon_like = 1;    //\n}\n\n// 动态-附加卡-电竞卡-战队\nmessage MatchTeam {\n  // 战队ID\n  int64 id = 1;\n  // 战队名\n  string name = 2;\n  // 战队图标\n  string cover = 3;\n  // 日间色值\n  string color = 4;\n  // 夜间色值\n  string night_color = 5;\n}\n\n// 动态列表渲染部分-详情模块-小程序/小游戏\nmessage MdlDynApplet {\n  // 小程序id\n  int64 id = 1;\n  // 跳转地址\n  string uri = 2;\n  // 主标题\n  string title = 4;\n  // 副标题\n  string sub_title = 5;\n  // 封面图\n  string cover = 6;\n  // 小程序icon\n  string icon = 7;\n  // 小程序标题\n  string label = 8;\n  // 按钮文案\n  string button_title = 9;\n}\n\n// 动态-详情模块-稿件\nmessage MdlDynArchive {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 秒开地址\n  string uri = 3;\n  // 视频封面展示项 1\n  string cover_left_text_1 = 4;\n  // 视频封面展示项 2\n  string cover_left_text_2 = 5;\n  // 封面视频展示项 3\n  string cover_left_text_3 = 6;\n  // avid\n  int64 avid = 7;\n  // cid\n  int64 cid = 8;\n  // 视频源类型\n  MediaType media_type = 9;\n  // 尺寸信息\n  Dimension dimension = 10;\n  // 角标，多个角标之前有间距\n  repeated VideoBadge badge = 11;\n  // 是否能够自动播放\n  bool  can_play = 12;\n  // stype\n  VideoType stype = 13;\n  // 是否PGC\n  bool isPGC = 14;\n  // inline播放地址\n  string inlineURL = 15;\n  // PGC的epid\n  int64 EpisodeId = 16;\n  // 子类型\n  int32 SubType = 17;\n  // PGC的ssid\n  int64 PgcSeasonId = 18;\n  // 播放按钮\n  string play_icon = 19;\n  // 时长\n  int64 duration = 20;\n  // 跳转地址\n  string jump_url = 21;\n  // 番剧是否为预览视频\n  bool is_preview = 22;\n  // 新角标，多个角标之前没有间距\n  repeated VideoBadge badge_category = 23;\n  // 当前是否是pgc正片\n  bool is_feature = 24;\n  // 是否是预约召回\n  ReserveType reserve_type = 25;\n  // bvid\n  string bvid = 26;\n  // 播放数\n  int64 view = 27;\n}\n\n// 动态列表渲染部分-详情模块-专栏模块\nmessage MdlDynArticle {\n  // 专栏id\n  int64 id = 1;\n  // 跳转地址\n  string uri = 2;\n  // 标题\n  string title = 3;\n  // 文案部分\n  string desc = 4;\n  // 配图\n  repeated string covers = 5;\n  // 阅读量标签\n  string label = 6;\n  // 模板类型\n  int32 templateID = 7;\n}\n\n// 动态列表渲染部分-详情模块-通用\nmessage MdlDynCommon {\n  // 物料id\n  int64 oid = 1;\n  // 跳转地址\n  string uri = 2;\n  // 标题\n  string title = 3;\n  // 描述 漫画卡标题下第一行\n  string desc = 4;\n  // 封面\n  string cover = 5;\n  // 标签1 漫画卡标题下第二行\n  string label = 6;\n  // 所属业务类型\n  int32 bizType = 7;\n  // 镜像数据ID\n  int64 sketchID = 8;\n  // 卡片样式\n  MdlDynCommonType style = 9;\n  // 角标\n  repeated VideoBadge badge = 10;\n}\n\n//\nenum MdlDynCommonType {\n  mdl_dyn_common_none = 0;    //\n  mdl_dyn_common_square = 1;  //\n  mdl_dyn_common_vertica = 2; //\n}\n\n// 动态-详情模块-付费课程批次\nmessage MdlDynCourBatch {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 跳转地址\n  string uri = 3;\n  // 展示项 1(本集标题)\n  string text_1 = 4;\n  // 展示项 2(更新了多少个视频)\n  string text_2 = 5;\n  // 角标\n  VideoBadge badge = 6;\n  // 播放按钮\n  string play_icon = 7;\n}\n\n// 动态-详情模块-付费课程系列\nmessage MdlDynCourSeason {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 跳转地址\n  string uri = 3;\n  // 展示项 1(更新信息)\n  string text_1 = 4;\n  // 描述信息\n  string desc = 5;\n  // 角标\n  VideoBadge badge = 6;\n  // 播放按钮\n  string play_icon = 7;\n}\n\n// 动态列表渲染部分-详情模块-图文模块\nmessage MdlDynDraw {\n  // 图片\n  repeated MdlDynDrawItem items = 1;\n  // 跳转地址\n  string uri = 2;\n  // 图文ID\n  int64 id = 3;\n}\n\n// 动态列表渲染部分-详情模块-图文\nmessage MdlDynDrawItem {\n  // 图片链接\n  string src = 1;\n  // 图片宽度\n  int64 width = 2;\n  // 图片高度\n  int64 height = 3;\n  // 图片大小\n  float size = 4;\n  // 图片标签\n  repeated MdlDynDrawTag tags = 5;\n}\n\n// 动态列表渲染部分-详情模块-图文-标签\nmessage MdlDynDrawTag {\n  // 标签类型\n  MdlDynDrawTagType type = 1;\n  // 标签详情\n  MdlDynDrawTagItem item = 2;\n}\n\n// 动态列表部分-详情模块-图文-标签详情\nmessage MdlDynDrawTagItem {\n  // 跳转链接\n  string url = 1;\n  // 标签内容\n  string text = 2;\n  // 坐标-x\n  int64 x = 3;\n  // 坐标-y\n  int64 y = 4;\n  // 方向\n  int32 orientation = 5;\n  // 来源\n  // 0:未知 1:淘宝 2:自营\n  int32 source = 6;\n  // 商品id\n  int64 item_id = 7;\n  // 用户mid\n  int64 mid = 8;\n  // 话题id\n  int64 tid = 9;\n  // lbs信息\n  string poi = 10;\n  // 商品标签链接\n  string schema_url = 11;\n}\n\n// 图文标签类型\nenum MdlDynDrawTagType {\n  mdl_draw_tag_none = 0;   // 占位\n  mdl_draw_tag_common = 1; // 普通标签\n  mdl_draw_tag_goods = 2;  // 商品标签\n  mdl_draw_tag_user = 3;   // 用户昵称\n  mdl_draw_tag_topic = 4;  // 话题名称\n  mdl_draw_tag_lbs = 5;    // lbs标签\n}\n\n// 动态列表渲染部分-详情模块-转发模块\nmessage MdlDynForward {\n  // 动态转发核心模块 套娃\n  DynamicItem item = 1;\n  // 透传类型\n  // 0:分享 1:转发\n  int32 rtype = 2;\n}\n\n// 动态列表渲染部分-详情模块-直播\nmessage MdlDynLive {\n  // 房间号\n  int64 id = 1;\n  // 跳转地址\n  string uri = 2;\n  // 直播间标题\n  string title = 3;\n  // 直播间封面\n  string cover = 4;\n  // 标题1 例: 陪伴学习\n  string cover_label = 5;\n  // 标题2 例: 54.6万人气\n  string cover_label2 = 6;\n  // 直播状态\n  LiveState live_state = 7;\n  // 直播角标\n  VideoBadge badge = 8;\n  // 是否是预约召回\n  ReserveType reserve_type = 9;\n}\n\n// 动态列表渲染部分-详情模块-直播推荐\nmessage MdlDynLiveRcmd {\n  // 直播数据\n  string content = 1;\n  // 是否是预约召回\n  ReserveType reserve_type = 2;\n}\n\n// 动态列表渲染部分-详情模块-播单\nmessage MdlDynMedialist {\n  // 播单id\n  int64 id = 1;\n  // 跳转地址\n  string uri = 2;\n  // 主标题\n  string title = 3;\n  // 副标题\n  string sub_title = 4;\n  // 封面图\n  string cover = 5;\n  // 封面类型\n  int32 cover_type = 6;\n  // 角标\n  VideoBadge badge = 7;\n}\n\n// 动态列表渲染部分-详情模块-音频模块\nmessage MdlDynMusic {\n  // 音频id\n  int64 id = 1;\n  // 跳转地址\n  string uri = 2;\n  // upId\n  int64 up_id = 3;\n  // 歌名\n  string title = 4;\n  // 专辑封面\n  string cover = 5;\n  // 展示项1\n  string label1 = 6;\n  // upper\n  string upper = 7;\n}\n\n// 动态-详情模块-pgc\nmessage MdlDynPGC {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 秒开地址\n  string uri = 3;\n  // 视频封面展示项 1\n  string cover_left_text_1 = 4;\n  // 视频封面展示项 2\n  string cover_left_text_2 = 5;\n  // 封面视频展示项 3\n  string cover_left_text_3 = 6;\n  // cid\n  int64 cid = 7;\n  // season_id\n  int64 season_id = 8;\n  // epid\n  int64 epid = 9;\n  // aid\n  int64 aid = 10;\n  // 视频源类型\n  MediaType media_type = 11;\n  // 番剧类型\n  VideoSubType sub_type = 12;\n  // 番剧是否为预览视频\n  bool is_preview = 13;\n  // 尺寸信息\n  Dimension dimension = 14;\n  // 角标，多个角标之前有间距\n  repeated VideoBadge badge = 15;\n  // 是否能够自动播放\n  bool  can_play = 16;\n  // season\n  PGCSeason season = 17;\n  // 播放按钮\n  string play_icon = 18;\n  // 时长\n  int64 duration = 19;\n  // 跳转地址\n  string jump_url = 20;\n  // 新角标，多个角标之前没有间距\n  repeated VideoBadge badge_category = 21;\n  // 当前是否是pgc正片\n  bool is_feature = 22;\n}\n\n// 动态列表渲染部分-详情模块-订阅卡\nmessage MdlDynSubscription {\n  // 卡片物料id\n  int64 id = 1;\n  // 广告创意id\n  int64 ad_id = 2;\n  // 跳转地址\n  string uri = 3;\n  // 标题\n  string title = 4;\n  // 封面图\n  string cover = 5;\n  // 广告标题\n  string ad_title = 6;\n  // 角标\n  VideoBadge badge = 7;\n  // 小提示\n  string tips = 8;\n}\n\n// 动态新附加卡\nmessage MdlDynSubscriptionNew {\n  //样式类型\n  MdlDynSubscriptionNewStyle style = 1;\n  // 新订阅卡数据\n  oneof item {\n    //\n    MdlDynSubscription dyn_subscription = 2;\n    // 直播推荐\n    MdlDynLiveRcmd dyn_live_rcmd = 3;\n  }\n}\n\n//\nenum MdlDynSubscriptionNewStyle {\n  mdl_dyn_subscription_new_style_nont = 0; // 占位\n  mdl_dyn_subscription_new_style_live = 1; // 直播\n  mdl_dyn_subscription_new_style_draw = 2; // 图文\n}\n\n// 动态列表渲染部分-UGC合集\nmessage MdlDynUGCSeason {\n  // 标题\n  string title = 1;\n  // 封面图\n  string cover = 2;\n  // 秒开地址\n  string uri = 3;\n  // 视频封面展示项 1\n  string cover_left_text_1 = 4;\n  // 视频封面展示项 2\n  string cover_left_text_2 = 5;\n  // 封面视频展示项 3\n  string cover_left_text_3 = 6;\n  // 卡片物料id\n  int64 id = 7;\n  // inline播放地址\n  string inlineURL = 8;\n  // 是否能够自动播放\n  bool  can_play = 9;\n  // 播放按钮\n  string play_icon = 10;\n  // avid\n  int64 avid = 11;\n  // cid\n  int64 cid = 12;\n  // 尺寸信息\n  Dimension dimension = 13;\n  // 时长\n  int64 duration = 14;\n  // 跳转地址\n  string jump_url = 15;\n}\n\n// 播放器类型\nenum MediaType {\n  MediaTypeNone = 0; // 本地\n  MediaTypeUGC = 1;  // UGC\n  MediaTypePGC = 2;  // PGC\n  MediaTypeLive = 3; // 直播\n  MediaTypeVCS = 4;  // 小视频\n}\n\n// 查看更多-列表单条数据\nmessage MixUpListItem {\n  // 用户mid\n  int64 uid = 1;\n  // 特别关注\n  // 0:否 1:是\n  int32 special_attention = 2;\n  // 小红点状态\n  // 0:没有 1:有\n  int32 reddot_state = 3;\n  // 直播信息\n  MixUpListLiveItem live_info = 4;\n  // 昵称\n  string name = 5;\n  // 头像\n  string face = 6;\n  // 认证信息\n  OfficialVerify official = 7;\n  // 大会员信息\n  VipInfo vip = 8;\n  // 关注状态\n  Relation relation = 9;\n}\n\nmessage MixUpListLiveItem {\n  // 直播状态\n  // 0:未直播 1:直播中\n  bool status = 1;\n  // 房间号\n  int64 room_id = 2;\n  // 跳转地址\n  string uri = 3;\n}\n\n// 动态模块\nmessage Module {\n  // 类型\n  DynModuleType module_type = 1;\n  oneof module_item {\n    // 用户模块 1\n    ModuleAuthor module_author = 2;\n    // 争议黄条模块 2\n    ModuleDispute module_dispute = 3;\n    // 动态正文模块 3\n    ModuleDesc module_desc = 4;\n    // 动态卡模块 4\n    ModuleDynamic module_dynamic = 5;\n    // 点赞外露(废弃)\n    ModuleLikeUser module_likeUser = 6;\n    // 小卡模块 6\n    ModuleExtend module_extend = 7;\n    // 大卡模块 5\n    ModuleAdditional module_additional = 8;\n    // 计数模块 8\n    ModuleStat module_stat = 9;\n    // 折叠模块 9\n    ModuleFold module_fold = 10;\n    // 评论外露(废弃)\n    ModuleComment module_comment = 11;\n    // 外露交互模块(点赞、评论) 7\n    ModuleInteraction module_interaction = 12;\n    // 转发卡-原卡用户模块\n    ModuleAuthorForward module_author_forward = 13;\n    // 广告卡\n    ModuleAd module_ad = 14;\n    // 通栏\n    ModuleBanner module_banner = 15;\n    // 获取物料失败\n    ModuleItemNull module_item_null = 16;\n    // 分享组件\n    ModuleShareInfo module_share_info = 17;\n    // 相关推荐模块\n    ModuleRecommend module_recommend = 18;\n    // 顶部模块\n    ModuleTop module_top = 19;\n    // 底部模块\n    ModuleButtom module_buttom = 20;\n    // 转发卡计数模块\n    ModuleStat module_stat_forward = 21;\n  }\n}\n\n// 动态列表-用户模块-广告卡\nmessage ModuleAd {\n  // 广告透传信息\n  google.protobuf.Any source_content = 1;\n  // 用户模块\n  ModuleAuthor module_author = 2;\n}\n\n// 动态-附加卡模块\nmessage ModuleAdditional {\n  // 类型\n  AdditionalType type = 1;\n  oneof item {\n    // 废弃\n    AdditionalPGC pgc = 2;\n    //\n    AdditionGoods goods = 3;\n    // 废弃\n    AdditionVote vote = 4;\n    //\n    AdditionCommon common = 5;\n    //\n    AdditionEsport esport = 6;\n    // 投票\n    AdditionVote2 vote2 = 8;\n    //\n    AdditionUgc  ugc = 9;\n    // up主预约发布卡\n    AdditionUP up = 10;\n  }\n  // 附加卡物料ID\n  int64 rid = 7;\n}\n\n// 动态-发布人模块\nmessage ModuleAuthor {\n  // 用户mid\n  int64 mid = 1;\n  // 时间标签\n  string ptime_label_text = 2;\n  // 用户详情\n  UserInfo author = 3;\n  // 装扮卡片\n  DecorateCard decorate_card = 4;\n  // 点击跳转链接\n  string uri = 5;\n  // 右侧操作区域 - 三点样式\n  repeated ThreePointItem tp_list = 6;\n  // 右侧操作区域样式枚举\n  ModuleAuthorBadgeType badge_type = 7;\n  // 右侧操作区域 - 按钮样式\n  ModuleAuthorBadgeButton badge_button = 8;\n  // 是否关注\n  // 1:关注 0:不关注 默认0，注：点赞列表使用，其他场景不使用该字段\n  int32 attend = 9;\n  // 关注状态\n  Relation relation = 10;\n  // 右侧操作区域 - 提权样式\n  Weight weight = 11;\n}\n\n// 动态列表渲染部分-用户模块-按钮\nmessage ModuleAuthorBadgeButton {\n  // 图标\n  string icon = 1;\n  // 文案\n  string title = 2;\n  // 状态\n  int32 state = 3;\n  // 物料ID\n  int64 id = 4;\n}\n\n// 右侧操作区域样式枚举\nenum ModuleAuthorBadgeType {\n  module_author_badge_type_none = 0;       // 占位\n  module_author_badge_type_threePoint = 1; // 三点\n  module_author_badge_type_button = 2;     // 按钮类型\n  module_author_badge_type_weight = 3;     // 提权\n}\n\n// 动态列表-用户模块-转发模板\nmessage ModuleAuthorForward {\n  // 展示标题\n  repeated ModuleAuthorForwardTitle title = 1;\n  // 源卡片跳转链接\n  string url = 2;\n  // 用户uid\n  int64 uid = 3;\n  // 时间标签\n  string ptime_label_text = 4;\n  // 是否展示关注\n  bool show_follow = 5;\n  // 源up主头像\n  string face_url = 6;\n  // 双向关系\n  Relation relation = 7;\n  // 右侧操作区域 - 三点样式\n  repeated ThreePointItem tp_list = 8;\n}\n\n// 动态列表-用户模块-转发模板-title部分\nmessage ModuleAuthorForwardTitle {\n  // 文案\n  string text = 1;\n  // 跳转链接\n  string url = 2;\n}\n\n// 动态列表-通栏\nmessage ModuleBanner {\n  // 模块标题\n  string title = 1;\n  // 卡片类型\n  ModuleBannerType type = 2;\n  // 卡片\n  oneof item{\n    ModuleBannerUser user = 3;\n  }\n  // 不感兴趣文案\n  string dislike_text = 4;\n  // 不感兴趣图标\n  string dislike_icon = 5;\n}\n\n// 动态列表-通栏类型\nenum ModuleBannerType {\n  module_banner_type_none = 0; //\n  module_banner_type_user = 1; //\n}\n\n// 动态通栏-用户\nmessage ModuleBannerUser {\n  // 卡片列表\n  repeated ModuleBannerUserItem list = 1;\n}\n\n// 动态通栏-推荐用户卡\nmessage ModuleBannerUserItem {\n  // up主头像\n  string face = 1;\n  // up主昵称\n  string name = 2;\n  // up主uid\n  int64 uid = 3;\n  // 直播状态\n  LiveState live_state = 4;\n  // 认证信息\n  OfficialVerify official = 5;\n  // 大会员信息\n  VipInfo vip = 6;\n  // 标签信息\n  string label = 7;\n  // 按钮\n  AdditionalButton button = 8;\n  // 跳转地址\n  string uri = 9;\n}\n\n// 底部模块\nmessage ModuleButtom {\n  // 计数模块\n  ModuleStat module_stat = 1;\n}\n\n// 评论外露模块\nmessage ModuleComment {\n  // 评论外露展示项\n  repeated CmtShowItem cmtShowItem = 1;\n}\n\n// 动态-描述文字模块\nmessage ModuleDesc {\n  // 描述信息(已按高亮拆分)\n  repeated Description desc = 1;\n  // 点击跳转链接\n  string jump_uri = 2;\n  // 文本原本\n  string text = 3;\n}\n\n// 正文商品卡参数\nmessage ModuleDescGoods {\n  // 商品类型\n  // 1:淘宝 2:会员购\n  int32 source_type = 1;\n  // 跳转链接\n  string jump_url = 2;\n  // schema_url\n  string schema_url = 3;\n  // item_id\n  int64 item_id = 4;\n  // open_white_list\n  repeated string open_white_list = 5;\n  // use_web_v2\n  bool user_web_v2 = 6;\n  // ad mark\n  string ad_mark = 7;\n  // schemaPackageName(Android用)\n  string schema_package_name = 8;\n}\n\n// 动态-争议小黄条模块\nmessage ModuleDispute {\n  // 标题\n  string title = 1;\n  // 描述内容\n  string desc = 2;\n  // 跳转链接\n  string uri = 3;\n}\n\n// 动态-详情模块\nmessage ModuleDynamic {\n  // 类型\n  ModuleDynamicType type = 1;\n  oneof module_item {\n    //稿件\n    MdlDynArchive dyn_archive = 2;\n    //pgc\n    MdlDynPGC dyn_pgc = 3;\n    //付费课程-系列\n    MdlDynCourSeason dyn_cour_season = 4;\n    //付费课程-批次\n    MdlDynCourBatch dyn_cour_batch = 5;\n    //转发卡\n    MdlDynForward dyn_forward = 6;\n    //图文\n    MdlDynDraw dyn_draw = 7;\n    //专栏\n    MdlDynArticle dyn_article = 8;\n    //音频\n    MdlDynMusic dyn_music = 9;\n    //通用卡方\n    MdlDynCommon dyn_common = 10;\n    //直播卡\n    MdlDynLive dyn_common_live = 11;\n    //播单\n    MdlDynMedialist dyn_medialist = 12;\n    //小程序卡\n    MdlDynApplet dyn_applet = 13;\n    //订阅卡\n    MdlDynSubscription dyn_subscription = 14;\n    //直播推荐卡\n    MdlDynLiveRcmd dyn_live_rcmd = 15;\n    //UGC合集\n    MdlDynUGCSeason dyn_ugc_season = 16;\n    //订阅卡\n    MdlDynSubscriptionNew dyn_subscription_new = 17;\n  }\n}\n\n// 动态详情模块类型\nenum ModuleDynamicType {\n  mdl_dyn_archive = 0;           // 稿件\n  mdl_dyn_pgc = 1;               // pgc\n  mdl_dyn_cour_season = 2;       // 付费课程-系列\n  mdl_dyn_cour_batch = 3;        // 付费课程-批次\n  mdl_dyn_forward = 4;           // 转发卡\n  mdl_dyn_draw = 5;              // 图文\n  mdl_dyn_article = 6;           // 专栏\n  mdl_dyn_music = 7;             // 音频\n  mdl_dyn_common = 8;            // 通用卡方\n  mdl_dyn_live = 9;              // 直播卡\n  mdl_dyn_medialist = 10;        // 播单\n  mdl_dyn_applet = 11;           // 小程序卡\n  mdl_dyn_subscription = 12;     // 订阅卡\n  mdl_dyn_live_rcmd = 13;        // 直播推荐卡\n  mdl_dyn_ugc_season = 14;       // UGC合集\n  mdl_dyn_subscription_new = 15; // 订阅卡\n}\n\n// 动态-小卡模块\nmessage ModuleExtend {\n  // 详情\n  repeated ModuleExtendItem extend = 1;\n  // 模块整体跳转uri\n  string uri = 2; // 废弃\n}\n\n// 动态-拓展小卡模块\nmessage ModuleExtendItem {\n  // 类型\n  DynExtendType type = 1;\n  // 卡片详情\n  oneof extend {\n    // 废弃\n    ExtInfoTopic ext_info_topic = 2;\n    // 废弃\n    ExtInfoLBS ext_info_lbs = 3;\n    // 废弃\n    ExtInfoHot ext_info_hot = 4;\n    // 废弃\n    ExtInfoGame ext_info_game = 5;\n    //\n    ExtInfoCommon ext_info_common = 6;\n    //\n    ExtInfoOGV  ext_info_ogv = 7;\n  }\n}\n\n// 动态-折叠模块\nmessage ModuleFold {\n  // 折叠分类\n  FoldType fold_type = 1;\n  // 折叠文案\n  string text = 2;\n  // 被折叠的动态\n  string fold_ids = 3;\n  // 被折叠的用户信息\n  repeated UserInfo fold_users = 4;\n}\n\n// 外露交互模块\nmessage ModuleInteraction {\n  // 外露交互模块\n  repeated InteractionItem interaction_item = 1;\n}\n\n// 获取物料失败模块\nmessage ModuleItemNull {\n  // 图标\n  string icon = 1;\n  // 文案\n  string text = 2;\n}\n\n// 动态-点赞用户模块\nmessage ModuleLikeUser {\n  // 点赞用户\n  repeated LikeUser like_users = 1;\n  // 文案\n  string display_text = 2;\n}\n\n// 相关推荐模块\nmessage ModuleRecommend {\n  // 模块标题\n  string module_title = 1;\n  // 图片\n  string image = 2;\n  // 标签\n  string tag = 3;\n  // 标题\n  string title = 4;\n  // 跳转链接\n  string jump_url = 5;\n  // 序列化的广告信息\n  repeated google.protobuf.Any ad = 6;\n}\n\n// 分享模块\nmessage ModuleShareInfo {\n  // 展示标题\n  string title = 1;\n  // 分享组件列表\n  repeated ShareChannel share_channels = 2;\n  // share_origin\n  string share_origin = 3;\n  // 业务id\n  string oid = 4;\n  // sid\n  string sid = 5;\n}\n\n// 动态-计数模块\nmessage ModuleStat {\n  // 转发数\n  int64 repost = 1;\n  // 点赞数\n  int64 like = 2;\n  // 评论数\n  int64 reply = 3;\n  // 点赞拓展信息\n  LikeInfo like_info = 4;\n  // 禁评\n  bool no_comment = 5;\n  // 禁转\n  bool no_forward = 6;\n  // 点击评论跳转链接\n  string reply_url = 7;\n  // 禁评文案\n  string no_comment_text = 8;\n  // 禁转文案\n  string no_forward_text = 9;\n}\n\n// 顶部模块\nmessage ModuleTop {\n  // 三点模块\n  repeated ThreePointItem tp_list = 1;\n}\n\n// 认证名牌\nmessage Nameplate {\n  // nid\n  int64 nid = 1;\n  // 名称\n  string name = 2;\n  // 图片地址\n  string image = 3;\n  // 小图地址\n  string image_small = 4;\n  // 等级\n  string level = 5;\n  // 获取条件\n  string condition = 6;\n}\n\n// 最新ep\nmessage NewEP {\n  // 最新话epid\n  int32 id = 1;\n  // 更新至XX话\n  string index_show = 2;\n  // 更新剧集的封面\n  string cover = 3;\n}\n\n// 空响应\nmessage NoReply {\n\n}\n\n// 空请求\nmessage NoReq {\n\n}\n\n// 认证信息\nmessage OfficialVerify {\n  // 127:未认证 0:个人 1:机构\n  int32 type = 1;\n  // 认证描述\n  string desc = 2;\n  // 是否关注\n  int32 is_atten = 3;\n}\n\n// PGC单季信息\nmessage PGCSeason {\n  // 是否完结\n  int32 is_finish = 1;\n  // 标题\n  string title = 2;\n  // 类型\n  int32 type = 3;\n}\n\n//\nmessage PlayerArgs {\n  //\n  int64 qn = 1;\n  //\n  int64 fnver = 2;\n  //\n  int64 fnval = 3;\n  //\n  int64 force_host = 4;\n}\n\n// 秒开通用参数\nmessage PlayurlParam {\n  // 清晰度\n  int32 qn = 1;\n  // 流版本\n  int32 fnver = 2;\n  // 流类型\n  int32 fnval = 3;\n  // 是否强制使用域名\n  int32 force_host = 4;\n  // 是否4k\n  int32 fourk = 5;\n}\n\n// 推荐up主入参\nmessage RcmdUPsParam {\n  int64 dislike_ts = 1;\n}\n\n// 刷新方式\nenum Refresh {\n  refresh_new = 0;     // 刷新列表\n  refresh_history = 1; // 请求历史\n}\n\n// 关注关系\nmessage Relation {\n  // 关注状态\n  RelationStatus status = 1;\n  // 关注\n  int32 is_follow = 2;\n  // 被关注\n  int32 is_followed = 3;\n  // 文案\n  string title = 4;\n}\n\n// 关注关系 枚举\nenum RelationStatus {\n  // 1-未关注 2-关注 3-被关注 4-互相关注 5-特别关注\n  relation_status_none = 0;\n  relation_status_nofollow = 1;\n  relation_status_follow = 2;\n  relation_status_followed = 3;\n  relation_status_mutual_concern = 4;\n  relation_status_special = 5;\n}\n\n//\nenum ReserveType {\n  reserve_none = 0;   // 占位\n  reserve_recall = 1; // 预约召回\n}\n\n// 分享渠道组件\nmessage ShareChannel {\n  // 分享名称\n  string name = 1;\n  // 分享按钮图片\n  string image = 2;\n  // 分享渠道\n  string channel = 3;\n}\n\n// 排序类型\nmessage SortType {\n  // 排序策略\n  // 1:推荐排序 2:最常访问 3:最近关注\n  int32  sort_type = 1;\n  // 排序策略名称\n  string sort_type_name = 2;\n}\n\n// 三点-关注\nmessage ThreePointAttention {\n  // attention icon\n  string attention_icon = 1;\n  // 关注时显示的文案\n  string attention_text = 2;\n  // not attention icon\n  string not_attention_icon = 3;\n  // 未关注时显示的文案\n  string not_attention_text = 4;\n  // 当前关注状态\n  ThreePointAttentionStatus status = 5;\n}\n\n// 枚举-三点关注状态\nenum ThreePointAttentionStatus {\n  tp_not_attention = 0; //\n  tp_attention = 1;     //\n}\n\n// 三点-自动播放 旧版不维护\nmessage ThreePointAutoPlay {\n  // open icon\n  string open_icon = 1;\n  // 开启时显示文案\n  string open_text = 2;\n  // close icon\n  string close_icon = 3;\n  // 关闭时显示文案\n  string close_text = 4;\n  // 开启时显示文案v2\n  string open_text_v2 = 5;\n  // 关闭时显示文案v2\n  string close_text_v2 = 6;\n  // 仅wifi/免流 icon\n  string only_icon = 7;\n  // 仅wifi/免流 文案\n  string only_text = 8;\n  // open icon v2\n  string open_icon_v2 = 9;\n  // close icon v2\n  string close_icon_v2 = 10;\n}\n\n// 三点-默认结构(使用此背景、举报、删除)\nmessage ThreePointDefault {\n  // icon\n  string icon = 1;\n  // 标题\n  string title = 2;\n  // 跳转链接\n  string uri = 3;\n  // id\n  string id = 4;\n}\n\n// 三点-不感兴趣\nmessage ThreePointDislike {\n  // icon\n  string icon = 1;\n  // 标题\n  string title = 2;\n}\n\n// 三点-收藏\nmessage ThreePointFavorite {\n  // icon\n  string icon = 1;\n  // 标题\n  string title = 2;\n  // 物料ID\n  int64 id = 3;\n  // 是否订阅\n  bool is_favourite = 4;\n  // 取消收藏图标\n  string cancel_icon = 5;\n  // 取消收藏文案\n  string cancel_title = 6;\n}\n\n// 三点Item\nmessage ThreePointItem {\n  //类型\n  ThreePointType type = 1;\n  oneof item {\n    // 默认结构\n    ThreePointDefault default = 2;\n    // 自动播放\n    ThreePointAutoPlay auto_player = 3;\n    // 分享\n    ThreePointShare share = 4;\n    // 关注\n    ThreePointAttention attention = 5;\n    // 稍后在看\n    ThreePointWait wait = 6;\n    // 不感兴趣\n    ThreePointDislike dislike = 7;\n    // 收藏\n    ThreePointFavorite favorite = 8;\n  }\n}\n\n// 三点-分享\nmessage ThreePointShare {\n  // icon\n  string icon = 1;\n  // 标题\n  string title = 2;\n  // 分享渠道\n  repeated ThreePointShareChannel channel = 3;\n  // 分享渠道名\n  string channel_name = 4;\n}\n\n// 三点-分享渠道\nmessage ThreePointShareChannel {\n  // icon\n  string icon = 1;\n  // 名称\n  string title = 2;\n}\n\n// 三点类型\nenum ThreePointType {\n  tp_none = 0;           // 占位\n  background = 1;        // 使用此背景\n  auto_play = 2;         // 自动播放\n  share = 3;             // 分享\n  wait = 4;              // 稍后再播\n  attention = 5;         // 关注\n  report = 6;            // 举报\n  delete = 7;            // 删除\n  dislike = 8;           // 不感兴趣\n  favorite = 9;          // 收藏\n}\n\n// 三点-稍后在看\nmessage ThreePointWait {\n  // addition icon\n  string addition_icon = 1;\n  // 已添加时的文案\n  string addition_text = 2;\n  // no addition icon\n  string no_addition_icon = 3;\n  // 未添加时的文案\n  string no_addition_text = 4;\n  // avid\n  int64 id = 5;\n}\n\n//\nenum ThumbType {\n  cancel = 0; //\n  thumb = 1;  //\n}\n\n// 话题广场操作按钮\nmessage TopicButton {\n  // 按钮图标\n  string icon = 1;\n  // 按钮文案\n  string title = 2;\n  // 跳转\n  string jump_uri = 3;\n}\n\n// 综合页-话题广场\nmessage TopicList {\n  // 模块标题\n  string title = 1;\n  // 话题列表\n  repeated TopicListItem topic_list_item = 2;\n  // 发起活动\n  TopicButton act_button = 3;\n  // 查看更多\n  TopicButton more_button = 4;\n}\n\n// 综合页-话题广场-话题\nmessage TopicListItem {\n  // 前置图标\n  string icon = 1;\n  // 前置图标文案\n  string icon_title = 2;\n  // 话题id\n  int64 topic_id = 3;\n  // 话题名\n  string topic_name = 4;\n  // 跳转链接\n  string url = 5;\n  // 卡片位次\n  int64 pos = 6;\n}\n\n// 综合页-无关注列表\nmessage Unfollow {\n  // 标题展示文案\n  string title = 1;\n  // 无关注列表\n  repeated UnfollowUserItem list = 2;\n  // trackID\n  string TrackId = 3;\n}\n\n// 综合页-无关注列表\nmessage UnfollowUserItem {\n  // 是否有更新\n  bool has_update = 1;\n  // up主头像\n  string face = 2;\n  // up主昵称\n  string name = 3;\n  // up主uid\n  int64 uid = 4;\n  // 排序字段 从1开始\n  int32 pos = 5;\n  // 直播状态\n  LiveState live_state = 6;\n  // 认证信息\n  OfficialVerify official = 7;\n  // 大会员信息\n  VipInfo vip = 8;\n  // up介绍\n  string sign = 9;\n  // 标签信息\n  string label = 10;\n  // 按钮\n  AdditionalButton button = 11;\n}\n\n// 动态顶部up列表-up主信息\nmessage UpListItem {\n  // 是否有更新\n  bool has_update = 1;\n  // up主头像\n  string face = 2;\n  // up主昵称\n  string name = 3;\n  // up主uid\n  int64 uid = 4;\n  // 排序字段 从1开始\n  int64 pos = 5;\n  // 用户类型\n  UserItemType user_item_type = 6;\n  // 直播头像样式-日\n  UserItemStyle display_style_day = 7;\n  // 直播头像样式-夜\n  UserItemStyle display_style_night = 8;\n  // 直播埋点\n  int64 style_id = 9;\n  // 直播状态\n  LiveState live_state = 10;\n  // 分割线\n  bool separator = 11;\n  // 跳转\n  string uri = 12;\n  // UP主预约上报使用\n  bool is_recall = 13;\n}\n\n// 最常访问-查看更多\nmessage UpListMoreLabel {\n  // 文案\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n}\n\n// 用户信息\nmessage UserInfo {\n  // 用户mid\n  int64 mid = 1;\n  // 用户昵称\n  string name = 2;\n  // 用户头像\n  string face = 3;\n  // 认证信息\n  OfficialVerify official = 4;\n  // 大会员信息\n  VipInfo vip = 5;\n  // 直播信息\n  LiveInfo live = 6;\n  // 空间页跳转链接\n  string uri = 7;\n  // 挂件信息\n  UserPendant pendant = 8;\n  // 认证名牌\n  Nameplate nameplate = 9;\n}\n\n// 直播头像样式\nmessage UserItemStyle {\n  //\n  string rect_text = 1;\n  //\n  string rect_text_color = 2;\n  //\n  string rect_icon = 3;\n  //\n  string rect_bg_color = 4;\n  //\n  string outer_animation = 5;\n}\n\n// 用户类型\nenum UserItemType {\n  user_item_type_none = 0;        //\n  user_item_type_live = 1;        //\n  user_item_type_live_custom = 2; //\n  user_item_type_normal = 3;      //\n  user_item_type_extend = 4;      //\n}\n\n// 头像挂件信息\nmessage UserPendant {\n  // pid\n  int64 pid = 1;\n  // 名称\n  string name = 2;\n  // 图片链接\n  string image = 3;\n  // 有效期\n  int64 expire = 4;\n}\n\n// 角标信息\nmessage VideoBadge {\n  // 文案\n  string text = 1;\n  // 文案颜色-日间\n  string text_color = 2;\n  // 文案颜色-夜间\n  string text_color_night = 3;\n  // 背景颜色-日间\n  string bg_color = 4;\n  // 背景颜色-夜间\n  string bg_color_night = 5;\n  // 边框颜色-日间\n  string border_color = 6;\n  // 边框颜色-夜间\n  string border_color_night = 7;\n  // 样式\n  int32 bg_style = 8;\n  // 背景透明度-日间\n  int32 bg_alpha = 9;\n  // 背景透明度-夜间\n  int32 bg_alpha_night = 10;\n}\n\n// 番剧类型\nenum VideoSubType {\n  VideoSubTypeNone = 0;        // 没有子类型\n  VideoSubTypeBangumi = 1;     // 番剧\n  VideoSubTypeMovie = 2;       // 电影\n  VideoSubTypeDocumentary = 3; // 纪录片\n  VideoSubTypeDomestic = 4;    // 国创\n  VideoSubTypeTeleplay = 5;    // 电视剧\n}\n\n// 视频类型\nenum VideoType {\n  video_type_general = 0;  //普通视频\n  video_type_dynamic = 1;  //动态视频\n  video_type_playback = 2; //直播回放视频\n}\n\n// 大会员信息\nmessage VipInfo {\n  // 大会员类型\n  int32 Type = 1;\n  // 大会员状态\n  int32 status = 2;\n  // 到期时间\n  int64 due_date = 3;\n  // 标签\n  VipLabel label = 4;\n  // 主题\n  int32 theme_type = 5;\n  // 大会员角标\n  // 0:无角标 1:粉色大会员角标 2:绿色小会员角标\n  int32 avatar_subscript = 6;\n  // 昵称色值，可能为空，色值示例：#FFFB9E60\n  string nickname_color = 7;\n}\n\n// 大会员标签\nmessage VipLabel {\n  // 图片地址\n  string path = 1;\n  // 文本值\n  string text = 2;\n  // 对应颜色类型\n  string label_theme = 3;\n}\n\n// 状态\nenum VoteStatus {\n  normal = 0;    // 正常\n  anonymous = 1; // 匿名\n}\n\n// 提权样式\nmessage Weight {\n  // 提权展示标题\n  string title = 1;\n  // 下拉框内容\n  repeated WeightItem items = 2;\n  // icon\n  string icon = 3;\n}\n\n// 热门默认跳转按钮\nmessage WeightButton {\n  string jump_url = 1;\n  // 展示文案\n  string title = 2;\n}\n\n// 提权不感兴趣\nmessage WeightDislike {\n  // 负反馈业务类型 作为客户端调用负反馈接口的参数\n  string feed_back_type = 1;\n  // 展示文案\n  string title = 2;\n}\n\n// 提权样式\nmessage WeightItem {\n  // 类型\n  WeightType type = 1;\n  oneof item {\n    // 热门默认跳转按钮\n    WeightButton button = 2;\n    // 提权不感兴趣\n    WeightDislike dislike = 3;\n  }\n}\n\n// 枚举-提权类型\nenum WeightType {\n  weight_none = 0;    // 默认 占位\n  weight_dislike = 1; // 不感兴趣\n  weight_jump = 2;    // 跳链\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/dynamic/interfaces/feed/v1/api.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.main.dynamic.feed.v1;\n\noption java_multiple_files = true;\noption java_package = \"bilibili.main.dynamic.feed.v1\";\n\nimport \"bilibili/dynamic/common/dynamic.proto\";\n\n//\nservice Feed {\n  // 发布页预校验\n  rpc CreateInitCheck(CreateInitCheckReq) returns (bilibili.dynamic.CreateCheckResp);\n  //\n  rpc SubmitCheck(SubmitCheckReq) returns (SubmitCheckRsp);\n  // 创建动态\n  rpc CreateDyn(CreateDynReq) returns (bilibili.dynamic.CreateResp);\n  // 根据name取uid\n  rpc GetUidByName(bilibili.dynamic.GetUidByNameReq) returns (bilibili.dynamic.GetUidByNameRsp);\n  // at用户推荐列表\n  rpc AtList(bilibili.dynamic.AtListReq) returns (bilibili.dynamic.AtListRsp);\n  // at用户搜索列表\n  rpc AtSearch(bilibili.dynamic.AtSearchReq) returns (bilibili.dynamic.AtListRsp);\n  //\n  rpc ReserveButtonClick(ReserveButtonClickReq) returns (ReserveButtonClickResp);\n  //\n  rpc CreatePlusButtonClick(CreatePlusButtonClickReq) returns (CreatePlusButtonClickRsp);\n  //\n  rpc HotSearch(HotSearchReq) returns (HotSearchRsp);\n  //\n  rpc Suggest(SuggestReq) returns (SuggestRsp);\n  //\n  rpc DynamicButtonClick(DynamicButtonClickReq) returns (DynamicButtonClickRsp);\n  //\n  rpc CreatePermissionButtonClick(CreatePermissionButtonClickReq) returns (CreatePermissionButtonClickRsp);\n  //\n  rpc CreatePageInfos(CreatePageInfosReq) returns (CreatePageInfosRsp);\n}\n\n// 创建动态-请求\nmessage CreateDynReq {\n  // 用户创建接口meta信息\n  bilibili.dynamic.UserCreateMeta meta = 1;\n  // 发布的内容\n  bilibili.dynamic.CreateContent content = 2;\n  // 发布类型\n  bilibili.dynamic.CreateScene scene = 3;\n  // 图片内容\n  repeated bilibili.dynamic.CreatePic pics = 4;\n  // 转发源\n  bilibili.dynamic.DynIdentity repost_src = 5;\n  // 动态视频\n  bilibili.dynamic.CreateDynVideo video = 6;\n  // 通用模板类型：2048方图 2049竖图 其他值无效\n  int64 sketch_type = 7;\n  // 通用模板的元内容（网页内容）\n  bilibili.dynamic.Sketch sketch = 8;\n  // 小程序的内容\n  bilibili.dynamic.Program program = 9;\n  // 动态附加小卡\n  bilibili.dynamic.CreateTag dyn_tag = 10;\n  // 动态附加大卡\n  bilibili.dynamic.CreateAttachCard attach_card = 11;\n  // 特殊的创建选项\n  bilibili.dynamic.CreateOption option = 12;\n  //\n  bilibili.dynamic.CreateTopic topic = 13;\n  //\n  string upload_id = 14;\n}\n\n//\nmessage CreateInitCheckReq {\n  //\n  int32 scene = 1;\n  //\n  bilibili.dynamic.MetaDataCtrl meta = 2;\n  //\n  bilibili.dynamic.RepostInitCheck repost = 3;\n}\n\n//\nmessage CreatePageInfosReq {\n  //\n  int64 topic_id = 1;\n}\n\n//\nmessage CreatePageInfosRsp {\n  //\n  CreatePageTopicInfo topic = 1;\n}\n\n//\nmessage CreatePageTopicInfo {\n  //\n  int64 topic_id = 1;\n  //\n  string topic_name = 2;\n}\n\n//\nmessage CreatePermissionButtonClickReq {\n  //\n  DynamicButtonClickBizType type = 1;\n}\n\n//\nmessage CreatePermissionButtonClickRsp {\n\n}\n\n//\nmessage CreatePlusButtonClickReq {\n\n}\n\n//\nmessage CreatePlusButtonClickRsp {\n\n}\n\n//\nenum DynamicButtonClickBizType {\n  DYNAMIC_BUTTON_CLICK_BIZ_TYPE_NONE = 0; //\n  DYNAMIC_BUTTON_CLICK_BIZ_TYPE_LIVE = 1; //\n  DYNAMIC_BUTTON_CLICK_BIZ_TYPE_DYN_UP = 2; //\n}\n\n//\nmessage DynamicButtonClickReq {\n\n}\n\n//\nmessage DynamicButtonClickRsp {\n\n}\n\n//\nmessage HotSearchReq {\n\n}\n\n//\nmessage HotSearchRsp {\n  //\n  message Item {\n    //\n    string words = 1;\n  }\n  //\n  repeated Item items = 1;\n  //\n  string version = 2;\n}\n\n//\nmessage ReserveButtonClickReq {\n  //\n  int64 uid = 1;\n  //\n  int64 reserve_id = 2;\n  //\n  int64 reserve_total = 3;\n  //\n  int32 cur_btn_status = 4;\n  //\n  string spmid = 5;\n  //\n  int64 dyn_id = 6;\n  //\n  int64 dyn_type = 7;\n}\n\n//\nmessage ReserveButtonClickResp {\n  //\n  ReserveButtonStatus final_btn_status = 1;\n  //\n  ReserveButtonMode btn_mode = 2;\n  //\n  int64 reserve_update = 3;\n  //\n  string desc_update = 4;\n  //\n  bool has_activity = 5;\n  //\n  string activity_url = 6;\n  //\n  string toast = 7;\n}\n\n//\nenum ReserveButtonMode {\n  RESERVE_BUTTON_MODE_NONE = 0;      //\n  RESERVE_BUTTON_MODE_RESERVE = 1;   //\n  RESERVE_BUTTON_MODE_UP_CANCEL = 2; //\n}\n\n//\nenum ReserveButtonStatus {\n  RESERVE_BUTTON_STATUS_NONE = 0;    //\n  RESERVE_BUTTON_STATUS_UNCHECK = 1; //\n  RESERVE_BUTTON_STATUS_CHECK = 2;   //\n}\n\n//\nmessage SubmitCheckReq {\n  //\n  bilibili.dynamic.CreateContent content = 1;\n  //\n  repeated bilibili.dynamic.CreatePic pics = 2;\n}\n\n//\nmessage SubmitCheckRsp {\n\n}\n\n//\nmessage SuggestReq {\n  //\n  string s = 1;\n  //\n  int32 type = 2;\n}\n\n//\nmessage SuggestRsp {\n  //\n  repeated string list = 1;\n  //\n  string track_id = 2;\n  //\n  string version = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/gaia/gw/gw_api.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.gaia.gw;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n// 应用列表上报\nservice Gaia {\n  // 应用列表上报\n  rpc ExUploadAppList(GaiaEncryptMsgReq) returns (UploadAppListReply);\n  // 拉取rsa公钥\n  rpc ExFetchPublicKey(google.protobuf.Empty) returns (FetchPublicKeyReply);\n}\n\n// 待加密的pb对象\nmessage DeviceAppList {\n  // 上报类型\n  // first_installation:首次安装上报 first_open:每日启动上报\n  string source = 1;\n  // 安装的系统程序列表\n  repeated string system_app_list = 2;\n  //安装的用户程序列表\n  repeated string user_app_list = 3;\n}\n\n// 加密方式\nenum EncryptType {\n  INVALID_ENCRYPT_TYPE = 0; // 非法值\n  CLIENT_AES = 1;           // 同客户端人工约定AES加密私钥，存储在客户端\n  SERVER_RSA_AES = 2;       // 客户端随机生成一个用于AES加密的私钥，并用服务端下发的RSA公钥来加密\n}\n\n//\nmessage FetchPublicKeyReply {\n  // 版本号\n  string version = 1;\n  // RSA公钥\n  string public_key = 2;\n  // 公钥过期时间\n  int64 deadline = 3;\n}\n\n//\nmessage GaiaDeviceBasicInfo {\n  //平台&应用信息\n  string platform = 1;        //android/ios/web/h5;\n  string device = 2;          //运行设备, 用于区分不同的app, 见客户端传入的对应参数。对于苹果系统，device有效值为phone, pad；安卓无法区分phone和pad，留空即可。\n  string mobi_app = 3;        //包类型，用于区分不同的app, 见客户端传入的对应参数（mobi_app ）；对于web端请求，请传空\n  string origin = 4;           //客户端appkey, 用以区分不同的客户端，对应客户端请求参数中的appkey,如果无法获取可传空“”\n  string app_id = 5;          //app产品编号 //产品编号，由数据平台分配，粉=1，白=2，蓝=3，直播姬=4，HD=5，海外=6，OTT=7，漫画=8，TV野版=9，小视频=10，网易漫画=11，网易漫画lite=12，网易漫画HD=13,国际版=14\n\n  //应用的版本信息\n  string sdkver = 6;            // SDK版本号   \"sdkver\": \"2.6.6\"\n  string app_version = 7;       // app版本  \"app_version\":\"5.36.0\"\n  string app_version_code = 8;  // app版本号 \"app_version_code\":\"5360000\"\n  string build = 9;             // app版本号，见客户端传入的对应参数；对于web端请求，请传空\n\n  //渠道信息\n  string channel = 10;   //渠道标识，见客户端传入的对应参数；对于web端请求，请传空；对应chid\n\n  //机器硬件信息\n  string brand = 11;   //手机品牌，见客户端传入的对应参数；\n  string model = 12;    //手机型号，见客户端传入的对应参数\n  string osver = 13;   //系统版本，见客户端传入的对应参数\n  string user_agent = 14;\n\n  //设备标识信息\n  string buvid_local = 15;  //本地设备唯一标识\n  string buvid = 16;        //设备唯一标识\n\n  //登陆用户信息\n  string mid = 17;         //最后一次登陆用户的mid，如果无登陆信息，传0即可\n\n  //本次启动信息\n  int64  fts = 18; // app首次启动时间 \"fts\":1530447775661\n  int32  first = 19; // 是否首次启动 是：0 否：1\n\n  //网络相关的信息\n  string network = 20; // 网络连接方式, WIFI/CELLULAR/OFFLINE/OTHERNET/ETHERNET \"network\":\"WIFI\", ESS_NETWORK_STATE、ACCESS_WIFI_STATE\n}\n\n// 应用列表上报-请求\nmessage GaiaEncryptMsgReq {\n  // 上报头部\n  GaiaMsgHeader header = 1;\n  // 加密数据\n  bytes encrypt_payload = 2;\n}\n\n// 风控通用消息头\nmessage GaiaMsgHeader {\n  //加密类型\n  EncryptType encode_type = 1;\n  //类型\n  PayloadType payload_type = 2;\n  //RAS加密后的aes_key\n  bytes encoded_aes_key = 3;\n  //当前时间戳(ms)\n  int64 ts = 4;\n}\n\n// 负载类型\nenum PayloadType {\n  INVALID_PAYLOAD = 0; //非法值\n  DEVICE_APP_LIST = 1; //设备app列表，对应DeviceAppList\n}\n\n// 应用列表上报-响应\nmessage UploadAppListReply {\n  // 上报响应id\n  string trace_id = 1;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/im/interfaces/inner-interface/v1/api.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.im.interface.inner.interface.v1;\n\noption java_multiple_files = true;\noption java_package = \"bilibili.im.interfaces.inner.interface1.v1\";\n\n//\nservice InnerInterface {\n  //\n  rpc UpdateListInn(ReqOpBlacklist) returns(RspOpBlacklist);\n}\n\n//\nmessage BanUser {\n  // 用户mid\n  uint64 uid = 1;\n  // 封禁业务\n  int32 limit = 2;\n  // 封禁时间\n  int32 time = 3;\n  // 模式\n  // 1:add 2:remove\n  int32 mode = 4;\n}\n\n//\nmessage ReqOpBlacklist {\n  // 需要封禁/解封的用户信息\n  repeated BanUser ban_users = 1;\n}\n\n//\nmessage RspOpBlacklist {\n  //\n  repeated uint64 failed_users = 1;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/im/interfaces/v1/im.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.im.interface.v1;\n\noption java_multiple_files = true;\noption java_package = \"bilibili.im.interface1.v1\";\n\nimport \"bilibili/im/type/im.proto\";\n\n// 私信\nservice ImInterface {\n  // 发送消息\n  rpc SendMsg (ReqSendMsg) returns (RspSendMsg);\n  // 同步关系\n  rpc SyncRelation (ReqRelationSync) returns (RspRelationSync);\n  // 确认同步进度\n  rpc SyncAck (ReqSyncAck) returns (RspSyncAck);\n  // 同步版本拉取消息\n  rpc SyncFetchSessionMsgs (ReqSessionMsg) returns (RspSessionMsg);\n  // 拉取会话记录列表\n  rpc GetSessions (ReqGetSessions) returns (RspSessions);\n  // 拉取新消息\n  rpc NewSessions (ReqNewSessions) returns (RspSessions);\n  // 拉取已读消息\n  rpc AckSessions (ReqAckSessions) returns (RspSessions);\n  // 更新已读进度\n  rpc UpdateAck (ReqUpdateAck) returns (DummyRsp);\n  // 置顶聊天\n  rpc SetTop (ReqSetTop) returns (DummyRsp);\n  // 删除会话记录\n  rpc RemoveSession (ReqRemoveSession) returns (DummyRsp);\n  // 未读私信数\n  rpc SingleUnread (ReqSingleUnread) returns (RspSingleUnread);\n  // 我创建的应援团未读数\n  rpc MyGroupUnread (DummyReq) returns (RspMyGroupUnread);\n  // 未关注的人批量设置为已读\n  rpc UpdateUnflwRead (DummyReq) returns (DummyRsp);\n  // 应援团消息助手\n  rpc GroupAssisMsg (ReqGroupAssisMsg) returns (RspSessionMsg);\n  // 更新应援团小助手消息已拉取进度\n  rpc AckAssisMsg (ReqAckAssisMsg) returns (DummyRsp);\n  // 拉取会话详情\n  rpc SessionDetail (ReqSessionDetail) returns (bilibili.im.type.SessionInfo);\n  // 批量拉取会话详情\n  rpc BatchSessDetail (ReqSessionDetails) returns (RspSessionDetails);\n  // 批量删除会话\n  rpc BatchRmSessions (ReqBatRmSess) returns (DummyRsp);\n  // 拉取最近私信分享列表\n  rpc ShareList (ReqShareList) returns (RspShareList);\n  //\n  rpc SpecificSingleUnread (ReqSpecificSingleUnread) returns (RspSpecificSingleUnread);\n  //\n  rpc GetSpecificSessions (ReqGetSpecificSessions) returns (RspSessions);\n  //\n  rpc GetLiveInfo(ReqLiveInfo) returns (RspLiveInfo);\n  //\n  rpc GetTotalUnread(ReqTotalUnread) returns (RspTotalUnread);\n  //\n  rpc ShowClearUnreadUI(ReqShowClearUnreadUI) returns (RspShowClearUnreadUI);\n  //\n  rpc CloseClearUnreadUI(ReqCloseClearUnreadUI) returns (RspCloseClearUnreadUI);\n  //\n  rpc UpdateTotalUnread(ReqUpdateTotalUnread) returns (RspUpdateTotalUnread);\n}\n\n// 空请求\nmessage DummyReq {\n  //\n  uint32 idl = 1;\n}\n\n// 空响应\nmessage DummyRsp {\n  reserved 1;\n}\n\n// 表情资源信息\nmessage EmotionInfo {\n  // 表情\n  string text = 1;\n  // 表情url\n  string url = 2;\n  // 表情大小\n  // 0:未知 1:min 2:max\n  int32  size = 3;\n  // gif url\n  string gif_url = 4;\n}\n\n//\nenum ENUM_FOLD {\n  FOLD_NO = 0; //\n  FOLD_YES = 1; //\n  FOLD_UNKNOWN = 2; //\n}\n\n//\nenum ENUM_UNREAD_TYPE{\n  UNREAD_TYPE_ALL = 0; //\n  UNREAD_TYPE_FOLLOW = 1; //\n  UNREAD_TYPE_UNFOLLOW = 2; //\n  UNREAD_TYPE_DUSTBIN = 3; //\n}\n\n//\nmessage MsgDetail {\n  //\n  int64 msg_key = 1;\n  //\n  int64 seqno = 2;\n}\n\n//\nmessage MsgFeedUnreadRsp {\n  //\n  map<string, int64> unread = 1;\n}\n\n// 更新应援团小助手消息已拉取进度-请求\nmessage ReqAckAssisMsg {\n  //\n  uint64 ack_seqno = 1;\n}\n\n// 拉取已读消息-请求\nmessage ReqAckSessions {\n  //\n  uint64 begin_ts = 1;\n  //\n  uint32 end_ts = 2;\n  //\n  uint32 size = 3;\n}\n\n// 批量删除会话-请求\nmessage ReqBatRmSess {\n\n}\n\n//\nmessage ReqCloseClearUnreadUI {\n\n}\n\n//\nmessage ReqGetMsg {\n  //\n  int64 talker_id = 1;\n  //\n  int32 session_type = 2;\n  //\n  repeated MsgDetail msg_detail = 3;\n}\n\n// 拉取会话记录列表-请求\nmessage ReqGetSessions {\n  //\n  uint64 begin_ts = 1;\n  //\n  uint64 end_ts = 2;\n  //\n  uint32 size = 3;\n  //\n  uint32 session_type = 4;\n  //\n  uint32 unfollow_fold = 5;\n  //\n  uint32 group_fold = 6;\n  //\n  uint32 sort_rule = 7;\n  // 青少年模式\n  uint32 teenager_mode = 8;\n  // 课堂模式\n  uint32 lessons_mode = 9;\n}\n\n// -请求\nmessage ReqGetSpecificSessions {\n  // 具体会话详情\n  repeated SimpleSession talker_sessions = 1;\n}\n\n// 应援团消息助手-请求\nmessage ReqGroupAssisMsg {\n  //\n  uint64 client_seqno = 1;\n  //\n  uint32 size = 2;\n}\n\n//\nmessage ReqLiveInfo {\n  //\n  int64 uid = 1;\n  //\n  int64 talker_id = 2;\n}\n\n// 拉取新消息-请求\nmessage ReqNewSessions {\n  //\n  uint64 begin_ts = 1;\n  //\n  uint32 size = 2;\n  //\n  uint32 teenager_mode = 3;\n  // 课堂模式\n  uint32 lessons_mode = 4;\n}\n\n// 同步关系-请求\nmessage ReqRelationSync {\n  // 客户端当前seqno\n  uint64 client_relation_oplog_seqno = 1;\n}\n\n// 删除会话记录-请求\nmessage ReqRemoveSession {\n  //\n  uint64 talker_id = 1;\n  //\n  uint32 session_type = 2;\n}\n\n// 发送消息-请求\nmessage ReqSendMsg {\n  // 消息内容\n  bilibili.im.type.Msg msg = 1;\n  //\n  string cookie = 2;\n  //\n  string cookie2 = 3;\n  //\n  int32 error_code = 4;\n  //\n  string dev_id = 5;\n}\n\n// 拉取会话详情-请求\nmessage ReqSessionDetail {\n  //\n  uint64 talker_id = 1;\n  //\n  uint32 session_type = 2;\n  //\n  uint64 uid = 3;\n}\n\n// 批量拉取会话详情-请求\nmessage ReqSessionDetails {\n  // 会话详情请求列表\n  repeated ReqSessionDetail sess_ids = 1;\n}\n\n// 同步版本拉取消息-请求\nmessage ReqSessionMsg {\n  //\n  uint64 talker_id = 1;\n  //\n  int32 session_type = 2;\n  //\n  uint64 end_seqno = 3;\n  //\n  uint64 begin_seqno = 4;\n  //\n  int32 size = 5;\n  //\n  int32 order = 6;\n  //\n  string dev_id = 7;\n}\n\n// 置顶聊天-请求\nmessage ReqSetTop {\n  //\n  uint64 talker_id = 1;\n  //\n  uint32 session_type = 2;\n  //\n  // 0:置顶 1:取消置顶\n  uint32 op_type = 3;\n}\n\n// 拉取最近私信分享列表-请求\nmessage ReqShareList {\n  // 分页大小 最大20\n  int32 size = 1;\n}\n\n//\nmessage ReqShowClearUnreadUI {\n  //\n  int32 unread_type = 1;\n  //\n  int32 show_unfollow_list = 2;\n  //\n  int32 show_dustbin = 4;\n}\n\n// 未读私信数-请求\nmessage ReqSingleUnread {\n  //\n  int32 unread_type = 1;\n  //\n  int32 show_unfollow_list = 2;\n  //\n  int64 uid = 3;\n  //\n  int32 show_dustbin = 4;\n}\n\n// -请求\nmessage ReqSpecificSingleUnread {\n  // 具体会话详情\n  repeated SimpleSession talker_sessions = 1;\n}\n\n// 确认同步进度-请求\nmessage ReqSyncAck {\n  //\n  uint64 client_seqno = 1;\n}\n\n//\nmessage ReqTotalUnread {\n  //\n  int32 unread_type = 1;\n  //\n  int32 show_unfollow_list = 2;\n  //\n  int64 uid = 3;\n  //\n  int32 show_dustbin = 4;\n  //\n  int32 singleunread_on = 5;\n  //\n  int32 msgfeed_on = 6;\n  //\n  int32 sysup_on = 7;\n}\n\n// 更新已读进度-请求\nmessage ReqUpdateAck {\n  // 聊天对象uid，可以为用户id或者为群id\n  uint64 talker_id = 1;\n  // 会话类型\n  uint32 session_type = 2;\n  // 已读的最大seqno\n  uint64 ack_seqno = 3;\n}\n\n//\nmessage ReqUpdateIntercept {\n  //\n  int64 uid = 1;\n  //\n  int64 talker_id = 2;\n  //\n  int32 status = 3;\n}\n\n//\nmessage ReqUpdateTotalUnread {\n\n}\n\n//\nmessage RspCloseClearUnreadUI {\n\n}\n\n//\nmessage RspGetMsg {\n  //\n  repeated bilibili.im.type.Msg msg = 1;\n}\n\n//\nmessage RspLiveInfo {\n  //\n  int64 live_status = 1;\n  //\n  string jump_url = 2;\n}\n\n// 我创建的应援团未读数-响应\nmessage RspMyGroupUnread {\n  // 未读消息数\n  uint32 unread_count = 1;\n}\n\n// 同步关系-响应\nmessage RspRelationSync {\n  //\n  int32 full = 1;\n  // 增量日志\n  repeated bilibili.im.type.RelationLog relation_logs = 2;\n  // 全量列表\n  repeated bilibili.im.type.FriendRelation friend_list = 3;\n  // 服务器端最大的relation seqno\n  uint64 server_relation_oplog_seqno = 4;\n  // 全量列表\n  repeated bilibili.im.type.GroupRelation group_list = 5;\n}\n\n// 发送消息-响应\nmessage RspSendMsg {\n  //\n  uint64 msg_key = 1;\n  // 表情资源信息\n  repeated EmotionInfo e_infos = 2;\n  //\n  string msg_content = 3;\n  //\n  bilibili.im.type.KeyHitInfos key_hit_infos = 4;\n}\n\n// 批量拉取会话详情-响应\nmessage RspSessionDetails {\n  // 会话详情列表\n  repeated bilibili.im.type.SessionInfo sess_infos = 1;\n}\n\n// 同步版本拉取消息-响应\nmessage RspSessionMsg {\n  //\n  repeated bilibili.im.type.Msg messages = 1;\n  //\n  int32 has_more = 2;\n  //\n  uint64 min_seqno = 3;\n  //\n  uint64 max_seqno = 4;\n  // 表情资源信息\n  repeated EmotionInfo e_infos = 5;\n}\n\n// 拉取消息-响应\nmessage RspSessions {\n  //\n  repeated bilibili.im.type.SessionInfo session_list = 1;\n  //\n  uint32 has_more = 2;\n  // 标记反垃圾会话是否在清理中\n  bool anti_disturb_cleaning = 3;\n  // 当session_list为空时，会返回该字段用于判断通讯录是否为空，1表示空，0表示非空\n  int32 is_address_list_empty = 4;\n  //\n  map<int32, int64> system_msg = 5;\n  //\n  bool show_level = 6;\n}\n\n// 拉取最近私信分享列表-响应\nmessage RspShareList {\n  // 最近会话列表\n  repeated ShareSessionInfo session_list = 1;\n  //\n  int32 IsAddressListEmpty = 2;\n}\n\n//\nmessage RspShowClearUnreadUI {\n  //\n  bool display = 1;\n  //\n  string text = 2;\n}\n\n// 未读私信数-响应\nmessage RspSingleUnread {\n  // 未关注用户私信数\n  uint64 unfollow_unread = 1;\n  // 已关注用户私信数\n  uint64 follow_unread = 2;\n  // 未关注人列表是否有新业务通知\n  uint32 unfollow_push_msg = 3;\n  //\n  int32 dustbin_push_msg = 4;\n  //\n  int64 dustbin_unread = 5;\n  //\n  int64 biz_msg_unfollow_unread = 6;\n  //\n  int64 biz_msg_follow_unread = 7;\n}\n\n// -响应\nmessage RspSpecificSingleUnread {\n  // key -> 用户uid, value ->未读数\n  map <uint64, uint64> talkerUnreadCnt = 1;\n  // 总未读数\n  uint64 allUnreadCnt = 2;\n}\n\n// 确认同步进度-响应\nmessage RspSyncAck {\n\n}\n\n//\nmessage RspTotalUnread {\n  //\n  SessionSingleUnreadRsp session_single_unread = 1;\n  //\n  MsgFeedUnreadRsp msg_feed_unread = 2;\n  //\n  SysMsgInterfaceLastMsgRsp sys_msg_interface_last_msg = 3;\n  //\n  int32 total_unread = 4;\n}\n\n//\nmessage RspUpdateTotalUnread {\n\n}\n\n//\nenum SESSION_TYPE {\n  UNKNOWN = 0; //\n  UN_FOLD_SESSION = 1; //\n  UN_FOLLOW_SINGLE_SESSION = 2; //\n  MY_GROUP_SESSION = 3; //\n  ALL_SESSION = 4; //\n  DUSTBIN_SESSION = 5; //\n}\n\n//\nmessage SessionSingleUnreadRsp {\n  //\n  int64 unfollow_unread = 1;\n  //\n  int64 follow_unread = 2;\n  //\n  int32 unfollow_push_msg = 3;\n  //\n  int32 dustbin_push_msg = 4;\n  //\n  int64 dustbin_unread = 5;\n}\n\n// 会话信息，用于私信分享\nmessage ShareSessionInfo {\n  //\n  uint64 talker_id = 1;\n  //\n  string talker_uname = 2;\n  //\n  string talker_icon = 3;\n  // 认证信息\n  // -1: 无认证 0:个人认证 1:机构认证\n  int32 official_type = 4;\n}\n\n//\nmessage SimpleSession {\n  // 聊天对象uid，可以为用户id或者为群id\n  uint64 talker_id = 1;\n  // 会话类型\n  uint32 session_type = 2;\n}\n\n//\nmessage SysMsgInterfaceLastMsgRsp {\n  //\n  int32 unread = 1;\n  //\n  string title = 2;\n  //\n  string time = 3;\n  //\n  int64 id = 4;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/im/type/im.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.im.type;\n\noption java_multiple_files = true;\n\n//\nmessage AccountInfo {\n  //\n  string name = 1;\n  //\n  string pic_url = 2;\n}\n\n//\nenum CmdId {\n  EN_CMD_ID_INVALID = 0;       //非法cmd\n\n  // msg_svr\n  EN_CMD_ID_SEND_MSG = 200001;  // 发消息\n\n  // sync_msg_svr\n  EN_CMD_ID_SYNC_MSG = 500001;  // 同步消息\n  EN_CMD_ID_SYNC_RELATION = 500002;  // 同步相关链\n  EN_CMD_ID_SYNC_ACK = 500003;  // 客户端同步消息完成后，向服务器确认同步进度\n  EN_CMD_ID_SYNC_FETCH_SESSION_MSGS = 500006;  // 多端同步版本拉取消息\n\n  // session_svr\n  EN_CMD_ID_SESSION_SVR_GET_SESSIONS = 1000001; // 拉会话列表\n  EN_CMD_ID_SESSION_SVR_NEW_SESSIONS = 1000002; // 新消息到达时获取会话列表\n  EN_CMD_ID_SESSION_SVR_ACK_SESSIONS = 1000003; // 获取已读位置有更新的会话列表\n  EN_CMD_ID_SESSION_SVR_UPDATE_ACK = 1000004; // 更新已读进度\n  EN_CMD_ID_SESSION_SVR_SET_TOP = 1000005; // 置顶/取消置顶\n  EN_CMD_ID_SESSION_SVR_REMOVE_SESSION = 1000007; // 删除会话\n  EN_CMD_ID_SESSION_SVR_SINGLE_UNREAD = 1000008; // 单聊未读信息数\n  EN_CMD_ID_SESSION_SVR_MY_GROUP_UNREAD = 1000009; // 我创建的应援团未读数\n  EN_CMD_ID_SESSION_SVR_UPDATE_UNFLW_READ = 1000010; // 未关注的人批量设置为已读\n  EN_CMD_ID_SESSION_SVR_GROUP_ASSIS_MSG = 1000011; // 应援团消息助手\n  EN_CMD_ID_SESSION_SVR_ACK_ASSIS_MSG = 1000012; // 更新应援团小助手消息已拉取进度\n  EN_CMD_ID_SESSION_SVR_SESSION_DETAIL = 1000015; // 拉会话详情\n  EN_CMD_ID_SESSION_SVR_BATCH_SESS_DETAIL = 1000016; // 批量拉会话详情\n  EN_CMD_ID_SESSION_SVR_BATCH_RM_SESSIONS = 1000017; // 批量删除会话\n}\n\n//\nenum ENUM_BIZ_MSG_TYPE {\n  BIZ_MSG_TYPE_NORMAL = 0;     //\n  BIZ_MSG_TYPE_CARD_VIDEO = 1; //\n}\n\n//\nmessage FriendRelation {\n  // 用户mid\n  uint64 uid = 1;\n  // 用户昵称\n  string user_name = 2;\n  // 头像url\n  string face = 3;\n  // vip类型\n  // 0:无 1:月度大会员 2:年度大会员\n  uint32 vip_level = 4;\n}\n\n//\nmessage GroupRelation {\n  //\n  uint64 group_id = 1;\n  //\n  uint64 owner_uid = 2;\n  //\n  uint32 group_type = 3;\n  //\n  uint32 group_level = 4;\n  //\n  string group_cover = 5;\n  //\n  string group_name = 6;\n  //\n  string group_notice = 7;\n  //\n  int32 status = 8;\n  //\n  int32 member_role = 9;\n  //\n  string fans_medal_name = 10;\n  //\n  uint64 room_id = 11;\n}\n\n// 关键词高亮文本\nmessage HighText {\n  //\n  string title = 1;\n  //\n  string url = 2;\n  // 表示高亮文本应该高亮第几个匹配的文本，ps：比如，“有疑问请联系客服，联系客服时，请说明具体的情况”，一共有2个匹配的文本，要高亮第一个匹配的，则index=1\n  uint32 index = 3;\n}\n\n//\nmessage ImgInfo {\n  //\n  string url = 1;\n  //\n  int32 width = 2;\n  //\n  int32 height = 3;\n  //\n  string imageType = 4;\n}\n\n// 关键词命中信息\nmessage KeyHitInfos {\n  //\n  string toast = 1;\n  //\n  uint32 rule_id = 2;\n  //\n  repeated HighText high_text = 3;\n  //\n}\n\n//\nmessage Medal {\n  //\n  int64 uid = 1;\n  //\n  int32 medal_id = 2;\n  //\n  int32 level = 3;\n  //\n  string medal_name = 4;\n  //\n  int32 score = 5;\n  //\n  int32 intimacy = 6;\n  //\n  int32 master_status = 7;\n  //\n  int32 is_receive = 8;\n  //\n  int64 medal_color_start = 9;\n  //\n  int64 medal_color_end = 10;\n  //\n  int64 medal_color_border = 11;\n  //\n  int64 medal_color_name = 12;\n  //\n  int64 medal_color_level = 13;\n  //\n  int64 guard_level = 14;\n}\n\n//\nmessage Msg {\n  // 发送方mid\n  uint64 sender_uid = 1;\n  // 接收方类型\n  RecverType receiver_type = 2;\n  // 接收方mid\n  uint64 receiver_id = 3;\n  // 客户端的序列id,用于服务端去重\n  uint64 cli_msg_id = 4;\n  // 消息类型\n  MsgType msg_type = 5;\n  // 消息内容\n  string content = 6;\n  // 服务端的序列号x\n  uint64 msg_seqno = 7;\n  // 消息发送时间（服务端时间）\n  uint64 timestamp = 8;\n  // @用户列表\n  repeated uint64 at_uids = 9;\n  // 多人消息\n  repeated uint64 recver_ids = 10;\n  // 消息唯一标示\n  uint64 msg_key = 11;\n  // 消息状态\n  uint32 msg_status = 12;\n  // 是否为系统撤销\n  bool sys_cancel = 13;\n  // 通知码\n  string notify_code = 14;\n  // 消息来源\n  MsgSource msg_source = 15;\n  // 如果msg.content有表情字符，则该参数需要置为1\n  int32 new_face_version = 16;\n  // 命中关键词信息\n  KeyHitInfos key_hit_infos = 17;\n}\n\n// 消息来源\nenum MsgSource {\n  EN_MSG_SOURCE_UNKONW = 0;  //\n  EN_MSG_SOURCE_IOS = 1;  //\n  EN_MSG_SOURCE_ANDRIOD = 2;  //\n  EN_MSG_SOURCE_H5 = 3;  //\n  EN_MSG_SOURCE_PC = 4;  //\n  EN_MSG_SOURCE_BACKSTAGE = 5;  //\n  EN_MSG_SOURCE_BIZ = 6;  //\n  EN_MSG_SOURCE_WEB = 7;  //\n  EN_MSG_SOURCE_AUTOREPLY_BY_FOLLOWED = 8;  //\n  EN_MSG_SOURCE_AUTOREPLY_BY_RECEIVE_MSG = 9;  //\n  EN_MSG_SOURCE_AUTOREPLY_BY_KEYWORDS = 10; //\n  EN_MSG_SOURCE_AUTOREPLY_BY_VOYAGE = 11; //\n  EN_MSG_SOURCE_VC_ATTACH_MSG = 12; //\n};\n\n// 消息类型\nenum MsgType {\n  // 基础消息类型\n  EN_INVALID_MSG_TYPE = 0;  // 空空的~\n  EN_MSG_TYPE_TEXT = 1;  // 文本消息\n  EN_MSG_TYPE_PIC = 2;  // 图片消息\n  EN_MSG_TYPE_AUDIO = 3;  // 语音消息\n  EN_MSG_TYPE_SHARE = 4;  // 分享消息\n  EN_MSG_TYPE_DRAW_BACK = 5;  // 撤回消息\n  EN_MSG_TYPE_CUSTOM_FACE = 6;  // 自定义表情\n  EN_MSG_TYPE_SHARE_V2 = 7;  // 分享v2消息\n  EN_MSG_TYPE_SYS_CANCEL = 8;  // 系统撤销\n  EN_MSG_TYPE_MINI_PROGRAM = 9;  // 小程序\n\n  // 扩展消息类型\n  EN_MSG_TYPE_NOTIFY_MSG = 10; // 业务通知\n  EN_MSG_TYPE_VIDEO_CARD = 11; // 视频卡片\n  EN_MSG_TYPE_ARTICLE_CARD = 12; // 专栏卡片\n  EN_MSG_TYPE_PICTURE_CARD = 13; // 图片卡\n  EN_MSG_TYPE_COMMON_SHARE_CARD = 14; // 异形卡\n  EN_MSG_TYPE_BIZ_MSG_TYPE = 50; //\n  EN_MSG_TYPE_MODIFY_MSG_TYPE = 51; //\n\n  // 功能类系统消息类型\n  EN_MSG_TYPE_GROUP_MEMBER_CHANGED = 101; // 群成员变更\n  EN_MSG_TYPE_GROUP_STATUS_CHANGED = 102; // 群状态变更\n  EN_MSG_TYPE_GROUP_DYNAMIC_CHANGED = 103; // 群动态变更\n  EN_MSG_TYPE_GROUP_LIST_CHANGED = 104; // 群列表变更\n  EM_MSG_TYPE_FRIEND_LIST_CHANGED = 105; // 好友列表变更\n  EN_MSG_TYPE_GROUP_DETAIL_CHANGED = 106; // 群详情发生变化\n  EN_MSG_TYPE_GROUP_MEMBER_ROLE_CHANGED = 107; // 群成员角色发生变化\n  EN_MSG_TYPE_NOTICE_WATCH_LIST = 108; //\n  EN_MSG_TYPE_NOTIFY_NEW_REPLY_RECIEVED = 109; // 消息系统，收到新的reply\n  EN_MSG_TYPE_NOTIFY_NEW_AT_RECIEVED = 110; //\n  EN_MSG_TYPE_NOTIFY_NEW_PRAISE_RECIEVED = 111; //\n  EN_MSG_TYPE_NOTIFY_NEW_UP_RECIEVED = 112; //\n  EN_MSG_TYPE_NOTIFY_NEW_REPLY_RECIEVED_V2 = 113; //\n  EN_MSG_TYPE_NOTIFY_NEW_AT_RECIEVED_V2 = 114; //\n  EN_MSG_TYPE_NOTIFY_NEW_PRAISE_RECIEVED_V2 = 115; //\n  EN_MSG_TYPE_GROUP_DETAIL_CHANGED_MULTI = 116; // 群详情发生变化,多端同步版本需要即时消息，无需落地\n  EN_MSG_TYPE_GROUP_MEMBER_ROLE_CHANGED_MULTI = 117; // 群成员角色发生变化,多端同步版本需要即时消息，无需落地\n  EN_MSG_TYPE_NOTIFY_ANTI_DISTURB = 118; //\n\n  // 系统通知栏消息类型\n  EN_MSG_TYPE_SYS_GROUP_DISSOLVED = 201; // 群解散\n  EN_MSG_TYPE_SYS_GROUP_JOINED = 202; // 入群\n  EN_MSG_TYPE_SYS_GROUP_MEMBER_EXITED = 203; // 成员主动退群\n  EN_MSG_TYPE_SYS_GROUP_ADMIN_FIRED = 204; // 房管被撤\n  EN_MSG_TYPE_SYS_GROUP_MEMBER_KICKED = 205; // 成员被T\n  EN_MSG_TYPE_SYS_GROUP_ADMIN_KICK_OFF = 206; // 管理T人\n  EN_MSG_TYPE_SYS_GROUP_ADMIN_DUTY = 207; // 管理上任\n  EN_MSG_TYPE_SYS_GROUP_AUTO_CREATED = 208; // 自动创建\n  EN_MSG_TYPE_SYS_FRIEND_APPLY = 210; // 好友申请\n  EN_MSG_TYPE_SYS_FRIEND_APPLY_ACK = 211; // 好友申请通过\n  EN_MSG_TYPE_SYS_GROUP_APPLY_FOR_JOINING = 212; // 用户加群申请\n  EN_MSG_TYPE_SYS_GROUP_ADMIN_ACCEPTED_USER_APPLY = 213; // 通知管理员,有其他管理员已经同意用户加群\n\n  // 聊天窗口通知消息类型\n  EN_MSG_TYPE_CHAT_MEMBER_JOINED = 301; // 入群\n  EN_MSG_TYPE_CHAT_MEMBER_EXITED = 302; // 退群\n  EN_MSG_TYPE_CHAT_GROUP_FREEZED = 303; // 冻结\n  EN_MSG_TYPE_CHAT_GROUP_DISSOLVED = 304; // 解散\n  EN_MSG_TYPE_CHAT_GROUP_CREATED = 305; // 开通应援团\n  EN_MSG_TYPE_CHAT_POPUP_SESSION = 306; // 弹出会话\n}\n\n// 接收方类型\nenum RecverType {\n  EN_NO_MEANING = 0; //\n  EN_RECVER_TYPE_PEER = 1; // 单人\n  EN_RECVER_TYPE_GROUP = 2; // 群\n  EN_RECVER_TYPE_PEERS = 3; // 多人\n}\n\n//\nmessage RelationLog {\n  // 操作类型\n  RelationLogType log_type = 1;\n  // 操作seqno\n  uint64 oplog_seqno = 2;\n  // 好友信息\n  FriendRelation friend_relation = 3;\n  // 群信息\n  GroupRelation group_relation = 4;\n}\n\n//\nenum RelationLogType {\n  EN_INVALID_LOG_TYPE = 0; //\n  EN_ADD_FRIEND = 1; // 添加好友\n  EN_REMOVE_FRIEND = 2; // 删除好友\n  EN_JOIN_GROUP = 3; // 加入群\n  EN_EXIT_GROUP = 4; // 退出群\n}\n\n//\nenum SESSION_TYPE {\n  INVALID_SESSION_TYPE = 0; //\n  UN_FOLD_SESSION = 1; //\n  UN_FOLLOW_SINGLE_SESSION = 2; //\n  MY_GROUP_SESSION = 3; //\n  ALL_SESSION = 4; //\n}\n\n// 会话详情\nmessage SessionInfo {\n  //\n  uint64 talker_id = 1;\n  //\n  uint32 session_type = 2;\n  //\n  uint64 at_seqno = 3;\n  //\n  uint64 top_ts = 4;\n  //\n  string group_name = 5;\n  //\n  string group_cover = 6;\n  //\n  uint32 is_follow = 7;\n  //\n  uint32 is_dnd = 8;\n  //\n  uint64 ack_seqno = 9;\n  //\n  uint64 ack_ts = 10;\n  //\n  uint64 session_ts = 11;\n  //\n  uint32 unread_count = 12;\n  //\n  Msg last_msg = 13;\n  //\n  uint32 group_type = 14;\n  //\n  uint32 can_fold = 15;\n  //\n  uint32 status = 16;\n  //\n  uint64 max_seqno = 17;\n  // 会话是否有业务通知\n  uint32 new_push_msg = 18;\n  // 接收方是否设置接受推送\n  uint32 setting = 19;\n  //\n  uint32 is_guardian = 20;\n  //\n  int32 is_intercept = 21;\n  //\n  int32 is_trust = 22;\n  //\n  int32 system_msg_type = 23;\n  //\n  AccountInfo account_info = 24;\n  //\n  int32 live_status = 25;\n  //\n  int32 biz_msg_unread_count = 26;\n  //\n  UserLabel user_label = 27;\n}\n\n//\nmessage UserLabel {\n  //\n  int32 label_type = 1;\n  //\n  Medal medal = 2;\n  //\n  int32 guardian_relation = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/live/app/room/v1/room.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.live.app.room.v1;\n\noption java_multiple_files = true;\n\n//\nmessage GetStudioListReq {\n  //\n  int64 room_id = 1;\n}\n\n//\nmessage GetStudioListResp {\n  //\n  message Pendants {\n    //\n    Pendant frame = 1;\n    //\n    Pendant badge = 2;\n  }\n  //\n  message Pendant {\n    //\n    string name = 1;\n    //\n    int64 position = 2;\n    //\n    string value = 3;\n    //\n    string desc = 4;\n  }\n  //\n  message StudioMaster {\n    //\n    int64 uid = 1;\n    //\n    int64 room_id = 2;\n    //\n    string uname = 3;\n    //\n    string face = 4;\n    //\n    Pendants pendants = 5;\n    //\n    string tag = 6;\n    //\n    int64 tag_type = 7;\n  }\n  //\n  int64 status = 1;\n  //\n  repeated StudioMaster master_list = 2;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/live/general/interfaces/v1/interfaces.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.live.general.interfaces.v1;\n\noption java_multiple_files = true;\noption java_package = \"bilibili.live.general.interfaces.v1\";\n\n//\nmessage GetOnlineRankReq {\n  //\n  int64 ruid = 1;\n  //\n  int64 room_id = 2;\n  //\n  int64 page = 3;\n  //\n  int64 page_size = 4;\n  //\n  string platform = 5;\n}\n\n//\nmessage GetOnlineRankResp {\n  //\n  message OnlineRankItem {\n    //\n    int64 uid = 1;\n    //\n    string uname = 2;\n    //\n    string face = 3;\n    //\n    int64 continue_watch = 4;\n    //\n    MedalInfo medal_info = 5;\n    //\n    int64 guard_level = 6;\n  }\n  //\n  OnlineRankItem item = 1;\n  //\n  int64 online_num = 2;\n}\n\n//\nmessage MedalInfo {\n  //\n  int64 guard_level = 1;\n  //\n  int64 medal_color_start = 2;\n  //\n  int64 medal_color_end = 3;\n  //\n  int64 medal_color_border = 4;\n  //\n  string medal_name = 5;\n  //\n  int64 level = 6;\n  //\n  int64 target_id = 7;\n  //\n  int64 is_light = 8;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/main/common/arch/doll/v1/doll.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.main.common.arch.doll.v1;\n\noption java_multiple_files = true;\n\n//\nservice Echo {\n  //\n  rpc Ping(PingRequest) returns(PingResponse);\n  //\n  rpc Say(SayRequest) returns(SayResponse);\n  //\n  rpc Error(ErrorRequest) returns(ErrorResponse);\n}\n\n//\nmessage ErrorRequest {\n  //\n  int32  error = 2;\n  //\n  int64  time = 1;\n  //\n  int64  delay = 3;\n}\n\n//\nmessage ErrorResponse {\n  //\n  string host = 1;\n  //\n  int64  time = 3;\n}\n\n//\nmessage PingRequest {\n  //\n  int64  time = 1;\n}\n\n//\nmessage PingResponse {\n  //\n  string host = 1;\n  //\n  int64  time = 3;\n}\n\n//\nmessage SayRequest {\n  //\n  string content = 1;\n}\n\n//\nmessage SayResponse {\n  //\n  string host = 1;\n  //\n  string content = 2;\n  //\n  int64  time = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/main/community/reply/v1/reply.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.main.community.reply.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/pagination/pagination.proto\";\nimport \"google/protobuf/any.proto\";\n\n// 评论区\nservice Reply {\n  // 主评论列表接口\n  rpc MainList(MainListReq) returns (MainListReply);\n  // 二级评论明细接口\n  rpc DetailList(DetailListReq) returns (DetailListReply);\n  // 对话评论树接口\n  rpc DialogList(DialogListReq) returns (DialogListReply);\n  // 评论预览接口\n  rpc PreviewList(PreviewListReq) returns (PreviewListReply);\n  // 评论搜索item前置发布接口\n  rpc SearchItemPreHook(SearchItemPreHookReq) returns (SearchItemPreHookReply);\n  // 评论搜索插入项目接口\n  rpc SearchItem(SearchItemReq) returns (SearchItemReply);\n  // 评论at用户搜索接口\n  rpc AtSearch(AtSearchReq) returns (AtSearchReply);\n  // 查询单条评论接口\n  rpc ReplyInfo(ReplyInfoReq) returns (ReplyInfoReply);\n  // 用户回调上报接口\n  rpc UserCallback(UserCallbackReq) returns (UserCallbackReply);\n  // 评论分享材料接口\n  rpc ShareRepliesInfo(ShareRepliesInfoReq) returns (ShareRepliesInfoResp);\n  // 评论表情推荐列表接口\n  rpc SuggestEmotes(SuggestEmotesReq) returns (SuggestEmotesResp);\n}\n\n// 活动\nmessage Activity {\n  // 活动id\n  int64 activity_id = 1;\n  // 活动状态\n  // -1:待审 1:上线\n  int64 activity_state = 2;\n  // 参与活动的输入框文案\n  string activity_placeholder = 3;\n}\n\n// 文章项目\nmessage ArticleSearchItem {\n  // 标题\n  string title = 1;\n  // UP主昵称\n  string up_nickname = 2;\n  // 封面\n  repeated string covers = 3;\n}\n\n// 评论at用户搜索组\nmessage AtGroup {\n  // 组类型\n  int32 group_type = 1;\n  // 组标题\n  string group_name = 2;\n  // 评论at用户搜索列表\n  repeated AtItem items = 3;\n}\n\n// 评论at用户搜索条目\nmessage AtItem {\n  // 用户mid\n  int64 mid = 1;\n  // 用户名\n  string name = 2;\n  // 用户头像url\n  string face = 3;\n  // 用户是否关注\n  int32 fans = 4;\n  // 用户认证类型\n  int32 official_verify_type = 5;\n}\n\n// 评论at用户搜索-响应\nmessage AtSearchReply {\n  // 评论at用户搜索组\n  repeated AtGroup groups = 1;\n}\n\n// 评论at用户搜索-请求\nmessage AtSearchReq {\n  //\n  int64 mid = 1;\n  // 关键字\n  string keyword = 2;\n}\n\n// 广告\nmessage CM {\n  // 广告数据(需要解包)\n  google.protobuf.Any source_content = 1;\n}\n\n// 评论主体信息\nmessage Content {\n  // 评论文本\n  string message = 1;\n  // 需要渲染的用户转义\n  map<string, Member> menber = 2;\n  // 需要渲染的表情转义\n  map<string, Emote> emote = 3;\n  // 需要高亮的话题转义\n  map<string, Topic> topic = 4;\n  // 需要高亮的超链转义\n  map<string, Url> url = 5;\n  // 投票信息\n  Vote vote = 6;\n  // at到的用户mid列表\n  map<string, int64> at_name_to_mid = 7;\n  // 富文本\n  RichText rich_text = 8;\n  // 评论图片\n  repeated  Picture pictures = 9;\n}\n\n// 图片信息\nmessage Picture {\n  // 图片URL\n  string img_src = 1;\n  // 图片宽度\n  double img_width = 2;\n  // 图片高度\n  double img_height = 3;\n  // 图片大小，单位KB\n  double img_size = 4;\n}\n\n// 页面游标回复\nmessage CursorReply {\n  // 下页数据\n  int64 next = 1;\n  // 上页数据\n  int64 prev = 2;\n  // 是否到顶\n  bool isBegin = 3;\n  // 是否到底\n  bool isEnd = 4;\n  // 排序方式\n  // 2:时间 3:热度\n  Mode mode = 5;\n  // 当前排序mode在切换按钮上的展示文案\n  string mode_text = 6;\n}\n\n// 页面游标请求\nmessage CursorReq {\n  // 下页数据\n  int64 next = 1;\n  // 上页数据\n  int64 prev = 2;\n  // 排序方式\n  Mode mode = 4;\n}\n\n// 二级评论明细-响应\nmessage DetailListReply {\n  // 页面游标\n  CursorReply cursor = 1;\n  // 评论区显示控制字段\n  SubjectControl subject_control = 2;\n  // 根评论信息(带二级评论)\n  ReplyInfo root = 3;\n  // 评论区的活动\n  Activity activity = 4;\n  //\n  LikeInfo likes = 5;\n  // 排序方式\n  Mode mode = 6;\n  // 当前排序mode在切换按钮上的展示文案\n  string mode_text = 7;\n  // 分页\n  bilibili.pagination.FeedPaginationReply pagination_reply = 8;\n  //\n  string session_id = 9;\n}\n\n// 二级评论明细-请求\nmessage DetailListReq {\n  // 目标评论区id\n  int64 oid = 1;\n  // 目标评论区业务type\n  int64 type = 2;\n  // 根评论rpid\n  int64 root = 3;\n  // 目标评论rpid\n  int64 rpid = 4;\n  // 页面游标\n  CursorReq cursor = 5;\n  // 来源标识\n  DetailListScene scene = 6;\n  // 排序方式\n  Mode mode = 7;\n  // 分页\n  bilibili.pagination.FeedPagination pagination = 8;\n}\n\n// 来源标识\nenum DetailListScene {\n  REPLY = 0; // 评论区展开\n  MSG_FEED = 1; // 回复消息推送\n  NOTIFY = 2; //\n}\n\n// 对话评论树-响应\nmessage DialogListReply {\n  // 页面游标\n  CursorReply cursor = 1;\n  // 评论区显示控制字段\n  SubjectControl subject_control = 2;\n  // 子评论列表\n  repeated ReplyInfo replies = 3;\n  // 评论区的活动\n  Activity activity = 4;\n}\n\n// 对话评论树-请求\nmessage DialogListReq {\n  // 目标评论区id\n  int64 oid = 1;\n  // 目标评论区业务type\n  int64 type = 2;\n  // 根评论rpid\n  int64 root = 3;\n  // 对话评论rpid\n  int64 rpid = 4;\n  // 页面游标\n  CursorReq cursor = 5;\n}\n\n// 特效\nmessage Effects {\n  //\n  string preloading = 1;\n}\n\n// 表情项\nmessage Emote {\n  // 表情大小\n  // 1:小 2:大\n  int64 size = 1;\n  // 表情url\n  string url = 2;\n  //\n  string jump_url = 3;\n  //\n  string jump_title = 4;\n  //\n  int64 id = 5;\n  //\n  int64 package_id = 6;\n  //\n  string gif_url = 7;\n  //\n  string text = 8;\n}\n\n// 商品项目\nmessage GoodsSearchItem {\n  // 商品id\n  int64 id = 1;\n  // 商品名\n  string name = 2;\n  // 价钱\n  string price = 3;\n  // 收入\n  string income = 4;\n  // 图片\n  string img = 5;\n  // 标签\n  string label = 6;\n}\n\n//\nmessage LikeInfo {\n  //\n  message Item {\n    //\n    Member member = 1;\n  }\n  //\n  repeated Item items = 1;\n  //\n  string title = 2;\n}\n\n// 抽奖\nmessage Lottery {\n  // 抽奖id\n  int64 lottery_id = 1;\n  // 抽奖状态\n  // 0:未开奖 1:开奖中 2:已开奖\n  int64 lottery_status = 2;\n  // 抽奖人mid\n  int64 lottery_mid = 3;\n  // 开奖时间\n  int64 lottery_time = 4;\n  //\n  int64 oid = 5;\n  //\n  int64 type = 6;\n  // 发送时间\n  int64 ctime = 7;\n  // 抽奖评论正文\n  Content content = 8;\n  // 用户信息\n  Member member = 9;\n  // 评论条目控制字段\n  ReplyControl reply_control = 10;\n}\n\n// 主评论列表-响应\nmessage MainListReply {\n  // 页面游标\n  CursorReply cursor = 1;\n  // 评论列表\n  repeated ReplyInfo replies = 2;\n  // 评论区显示控制字段\n  SubjectControl subject_control = 3;\n  // UP置顶评论\n  ReplyInfo up_top = 4;\n  // 管理员置顶评论\n  ReplyInfo admin_top = 5;\n  // 投票置顶评论\n  ReplyInfo vote_top = 6;\n  // 评论区提示\n  Notice notice = 7;\n  // 抽奖评论\n  Lottery lottery = 8;\n  // 活动\n  Activity activity = 9;\n  // 精选评论区筛选后台信息\n  UpSelection up_selection = 10;\n  // 广告\n  CM cm = 11;\n  // 特效\n  Effects effects = 12;\n  //\n  Operation operation = 13;\n  //\n  repeated ReplyInfo top_replies = 14;\n  //\n  QoeInfo qoe = 15;\n  //\n  map<string, int32> callbacks = 16;\n  //\n  OperationV2 operation_v2 = 17;\n  // 排序方式\n  Mode mode = 18;\n  // 当前排序mode在切换按钮上的展示文案\n  string mode_text = 19;\n  // 分页\n  bilibili.pagination.FeedPaginationReply pagination_reply = 20;\n  //\n  string session_id = 21;\n  //\n  string report_params = 22;\n  //\n  VoteCard vote_card = 23;\n}\n\n// 主评论列表-请求\nmessage MainListReq {\n  // 目标评论区id\n  int64 oid = 1;\n  // 目标评论区业务type\n  int64 type = 2;\n  // 页面游标\n  CursorReq cursor = 3;\n  // 扩展数据json\n  string extra = 4;\n  // 广告扩展\n  string ad_extra = 5;\n  // 目标评论rpid\n  int64 rpid = 6;\n  //\n  int64 seek_rpid = 7;\n  // 评论区筛选类型 取值可为: [\"全部\" \"粉丝评论\" \"笔记长评\"]\n  string filter_tag_name = 8;\n  // 排序方式\n  Mode mode = 9;\n  // 分页\n  bilibili.pagination.FeedPagination pagination = 10;\n}\n\n// 用户信息\nmessage Member {\n  // 地区类型\n  enum RegionType {\n    DEFAULT = 0; // 默认\n    MAINLAND = 1; // 大陆地区\n    GAT = 2; //\n  }\n  //\n  enum ShowStatus {\n    SHOWDEFAULT = 0; // 默认\n    ZOOMINMAINLAND = 1; //\n    RAW = 2; //\n  }\n  // NFT地区\n  message Region {\n    // 地区类型\n    RegionType type = 1;\n    // 角标url\n    string icon = 2;\n    //\n    ShowStatus show_status = 3;\n  }\n  // NFT信息\n  message NftInteraction {\n    //\n    string itype = 1;\n    //\n    string metadata_url = 2;\n    //\n    string nft_id = 3;\n    // NFT地区\n    Region region = 4;\n  }\n  /**********基础信息**********/\n  // 用户mid\n  int64 mid = 1;\n  // 昵称\n  string name = 2;\n  // 性别\n  string sex = 3;\n  // 头像url\n  string face = 4;\n  // 等级\n  int64 level = 5;\n  // 认证类型\n  int64 official_verify_type = 6;\n  /**********VIP相关**********/\n  // 会员类型\n  // 0:不是大会员 1:月度会员 2:年度大会员\n  int64 vip_type = 7;\n  // 会员状态\n  int64 vip_status = 8;\n  // 会员样式\n  int64 vip_theme_type = 9;\n  // 会员铭牌样式url\n  string vip_label_path = 10;\n  /**********装扮相关**********/\n  // 头像框url\n  string garb_pendant_image = 11;\n  // 装扮卡url\n  string garb_card_image = 12;\n  // 有关注按钮时的装扮卡url\n  string garb_card_image_with_focus = 13;\n  // 专属装扮页面url\n  string garb_card_jump_url = 14;\n  // 专属装扮id\n  string garb_card_number = 15;\n  // 专属装扮id显示颜色\n  string garb_card_fan_color = 16;\n  // 是否为专属装扮卡\n  bool garb_card_is_fan = 17;\n  /**********粉丝勋章相关**********/\n  // 粉丝勋章名\n  string fans_medal_name = 18;\n  // 粉丝勋章等级\n  int64 fans_medal_level = 19;\n  // 粉丝勋章显示颜色\n  int64 fans_medal_color = 20;\n  // 会员昵称颜色\n  string vip_nickname_color = 21;\n  // 会员角标\n  // 0:无角标 1:粉色大会员角标 2:绿色小会员角标\n  int32 vip_avatar_subscript = 22;\n  // 会员标签文\n  string vip_label_text = 23;\n  // 会员标颜色\n  string vip_label_theme = 24;\n  // 粉丝勋章底色\n  int64 fans_medal_color_end = 25;\n  // 粉丝勋章边框颜色\n  int64 fans_medal_color_border = 26;\n  // 粉丝勋章名颜色\n  int64 fans_medal_color_name = 27;\n  // 粉丝勋章等级颜色\n  int64 fans_medal_color_level = 28;\n  //\n  int64 fans_guard_level = 29;\n  //\n  int32 face_nft = 30;\n  // 是否NFT头像\n  int32 face_nft_new = 31;\n  // 是否为硬核会员\n  int32 is_senior_member = 32;\n  // NFT信息\n  NftInteraction nft_interaction = 33;\n  //\n  string fans_guard_icon = 34;\n  //\n  string fans_honor_icon = 35;\n}\n\n// 用户信息V2\nmessage MemberV2 {\n  // 基本信息\n  message Basic {\n    // 用户mid\n    int64 mid = 1;\n    // 昵称\n    string name = 2;\n    // 性别\n    string sex = 3;\n    // 头像url\n    string face = 4;\n    // 等级\n    int64 level = 5;\n  }\n  // 认证\n  message Official {\n    // 认证类型\n    int64 verify_type = 1;\n  }\n  // 大会员\n  message Vip {\n    // 会员类型\n    // 0:不是大会员 1:月度会员 2:年度大会员\n    int64 type = 1;\n    // 会员状态\n    int64 status = 2;\n    // 会员样式\n    int64 theme_type = 3;\n    // 会员铭牌样式url\n    string label_path = 4;\n    //\n    string nickname_color = 5;\n    //\n    int32 avatar_subscript = 6;\n    //\n    string label_text = 7;\n    //\n    string vip_label_theme = 8;\n  }\n  // 装扮\n  message Garb {\n    // 头像框url\n    string pendant_image = 1;\n    // 装扮卡url\n    string card_image = 2;\n    // 有关注按钮时的装扮卡url\n    string card_image_with_focus = 3;\n    // 专属装扮页面url\n    string card_jump_url = 4;\n    // 专属装扮id\n    string card_number = 5;\n    // 专属装扮id显示颜色\n    string card_fan_color = 6;\n    // 是否为专属装扮卡\n    bool card_is_fan = 7;\n  }\n  // 粉丝勋章\n  message Medal {\n    // 粉丝勋章名\n    string name = 1;\n    // 粉丝勋章等级\n    int64 level = 2;\n    // 粉丝勋章显示颜色\n    int64 color_start = 3;\n    // 粉丝勋章底色\n    int64 color_end = 4;\n    // 粉丝勋章边框颜色\n    int64 color_border = 5;\n    // 粉丝勋章名颜色\n    int64 color_name = 6;\n    // 粉丝勋章等级颜色\n    int64 color_level = 7;\n    //\n    int64 guard_level = 8;\n    //\n    string first_icon = 9;\n    //\n    int64 level_bg_color = 11;\n  }\n  // 地区类型\n  enum RegionType {\n    DEFAULT = 0; // 默认\n    MAINLAND = 1; // 大陆地区\n    GAT = 2; //\n  }\n  //\n  enum ShowStatus {\n    SHOWDEFAULT = 0; //\n    ZOOMINMAINLAND = 1; //\n    RAW = 2; //\n  }\n  // NFT地区\n  message Region {\n    // 地区类型\n    RegionType type = 1;\n    // 角标url\n    string icon = 2;\n    //\n    ShowStatus show_status = 3;\n  }\n  // NFT信息\n  message Interaction {\n    //\n    string itype = 1;\n    //\n    string metadata_url = 2;\n    //\n    string nft_id = 3;\n    // NFT地区\n    Region region = 4;\n\n  }\n  // NFT\n  message Nft {\n    //\n    int32 face = 1;\n    //\n    Interaction interaction = 2;\n  }\n  // 硬核会员\n  message Senior {\n    // 是否为硬核会员\n    int32 is_senior_member = 1;\n  }\n  // 契约\n  message Contractor {\n    // 是否和up签订契约\n    bool is_contractor = 1;\n    // 契约显示文案\n    string contract_desc = 2;\n  }\n  // 基本信息\n  Basic basic = 1;\n  // 认证信息\n  Official official = 2;\n  // 大会员信息\n  Vip vip = 3;\n  // 装扮信息\n  Garb garb = 4;\n  // 粉丝勋章信息\n  Medal medal = 5;\n  // NFT信息\n  Nft nft = 6;\n  // 硬核会员信息\n  Senior senior = 7;\n  // 契约信息\n  Contractor contractor = 8;\n}\n\n// 排序方式\nenum Mode {\n  DEFAULT = 0; //\n  UNSPECIFIED = 1; // 默认排序\n  MAIN_LIST_TIME = 2; // 按时间\n  MAIN_LIST_HOT = 3; // 按热度\n}\n\n//\nmessage Notice {\n  //\n  int64 id = 1;\n  //\n  string content = 2;\n  //\n  string link = 3;\n}\n\n//\nmessage Operation {\n  //\n  int32 type = 1;\n  //\n  int64 id = 2;\n  //\n  OperationTitle title = 3;\n  //\n  OperationTitle subtitle = 4;\n  //\n  string link = 5;\n  //\n  string report_extra = 6;\n  //\n  string icon = 7;\n}\n\n//\nmessage OperationV2 {\n  //\n  int32 type = 1;\n  //\n  string prefix_text = 2;\n  //\n  OperationIcon icon = 3;\n  //\n  string title = 4;\n  //\n  string link = 5;\n  //\n  string report_extra = 6;\n}\n\n//\nmessage OperationIcon {\n  //\n  int32 position = 1;\n  //\n  string url = 2;\n}\n\n//\nmessage OperationTitle {\n  //\n  string content = 1;\n  //\n  bool is_highlight = 2;\n}\n\n// PGC视频项目\nmessage PGCVideoSearchItem {\n  // 标题\n  string title = 1;\n  // 类别\n  string category = 2;\n  // 封面\n  string cover = 3;\n}\n\n// 评论区预览-回复\nmessage PreviewListReply {\n  // 页面游标\n  CursorReply cursor = 1;\n  // 评论列表\n  repeated ReplyInfo replies = 2;\n  // 评论区显示控制字段\n  SubjectControl subject_control = 3;\n  // UP置顶评论\n  ReplyInfo upTop = 4;\n  // 管理员置顶评论\n  ReplyInfo admin_top = 5;\n  // 投票置顶评论\n  ReplyInfo vote_top = 6;\n}\n\n// 评论区预览-请求\nmessage PreviewListReq {\n  // 目标评论区id\n  int64 oid = 1;\n  // 目标评论区业务type\n  int64 type = 2;\n  // 页面游标\n  CursorReq cursor = 3;\n}\n\n//\nmessage QoeInfo {\n  //\n  int64 id = 1;\n  //\n  int32 type = 2;\n  //\n  int32 style = 3;\n  //\n  string title = 4;\n  //\n  string feedback_title = 5;\n  //\n  repeated QoeScoreItem score_items = 6;\n  //\n  int64 display_rank = 7;\n}\n\n//\nmessage QoeScoreItem {\n  //\n  string title = 1;\n  //\n  string url = 2;\n  //\n  float score = 3;\n}\n\n// 评论条目标签信息\nmessage ReplyCardLabel {\n  // 标签文本\n  string text_content = 1;\n  // 文本颜色\n  string text_color_day = 2;\n  // 文本颜色夜间\n  string text_color_night = 3;\n  // 标签颜色\n  string label_color_day = 4;\n  // 标签颜色夜间\n  string label_color_night = 5;\n  //\n  string image = 6;\n  // 标签类型 0:UP主觉得很赞 1:妙评\n  int32 type = 7;\n  // 背景url\n  string background = 8;\n  // 背景宽\n  double background_width = 9;\n  // 背景高\n  double background_height = 10;\n  // 点击跳转url\n  string jump_url = 11;\n  //\n  int64 effect = 12;\n  //\n  int64 effect_start_time = 13;\n}\n\n// 评论条目控制字段\nmessage ReplyControl {\n  // 操作行为标志\n  // 0:无 1:已点赞 2:已点踩\n  int64 action = 1;\n  // 是否UP觉得很赞\n  bool up_like = 2;\n  // 是否存在UP回复\n  bool up_reply = 3;\n  // 是否显示关注按钮\n  bool show_follow_btn = 4;\n  // 是否协管\n  bool is_assist = 5;\n  // 是否展示标签\n  string label_text = 6;\n  // 是否关注\n  bool following = 7;\n  // 是否粉丝\n  bool followed = 8;\n  // 是否被自己拉黑\n  bool blocked = 9;\n  // 是否存在折叠的二级评论\n  bool has_folded_reply = 10;\n  // 是否折叠\n  bool is_folded_reply = 11;\n  // 是否UP置顶\n  bool is_up_top = 12;\n  // 是否管理置顶\n  bool is_admin_top = 13;\n  // 是否置顶投票评论\n  bool is_vote_top = 14;\n  // 最大收起显示行数\n  int64 max_line = 15;\n  // 该条评论可不可见\n  bool invisible = 16;\n  // 是否和up签订契约\n  bool is_contractor = 17;\n  // 是否是笔记评论\n  bool is_note = 18;\n  // 评论条目标签列表\n  repeated ReplyCardLabel card_labels = 19;\n  // 子评论数文案 \"共x条回复\"\n  string sub_reply_entry_text = 20;\n  // 子评论数文案 \"相关回复共x条\"\n  string sub_reply_title_text = 21;\n  // 契约显示文案\n  string contract_desc = 22;\n  // 发布时间文案 \"x天前发布\"\n  string time_desc = 23;\n  //\n  string biz_scene = 24;\n  // IP属地信息 \"IP属地：xxx\"\n  string location = 25;\n}\n\n//\nmessage ReplyExtra {\n  //\n  int64 season_id = 1;\n  //\n  int64 season_type = 2;\n  //\n  int64 ep_id = 3;\n  //\n  bool is_story = 4;\n}\n\n// 评论条目信息\nmessage ReplyInfo {\n  // 二级评论列表\n  repeated ReplyInfo replies = 1;\n  // 评论rpid\n  int64 id = 2;\n  // 评论区对象id\n  int64 oid = 3;\n  // 评论区类型\n  int64 type = 4;\n  // 发布者UID\n  int64 mid = 5;\n  // 根评论rpid\n  int64 root = 6;\n  // 父评论rpid\n  int64 parent = 7;\n  // 对话评论rpid\n  int64 dialog = 8;\n  // 点赞数\n  int64 like = 9;\n  // 发布时间\n  int64 ctime = 10;\n  // 回复数\n  int64 count = 11;\n  // 评论主体信息\n  Content content = 12;\n  // 发布者信息\n  Member member = 13;\n  // 评论控制字段\n  ReplyControl reply_control = 14;\n  // 发布者信息V2\n  MemberV2 member_v2 = 15;\n}\n\n// 查询单条评论-响应\nmessage ReplyInfoReply {\n  // 评论条目信息\n  ReplyInfo reply = 1;\n}\n\n// 查询单条评论-请求\nmessage ReplyInfoReq {\n  // 评论rpid\n  int64 rpid = 1;\n  //\n  int32 scene = 2;\n}\n\n// 富文本\nmessage RichText {\n  // 富文本类型\n  oneof item {\n    // 笔记\n    RichTextNote note = 1;\n  }\n}\n\n// 笔记\nmessage RichTextNote {\n  // 预览文本\n  string summary = 1;\n  // 笔记预览图片url列表\n  repeated string images = 2;\n  // 笔记页面url\n  string click_url = 3;\n  // 发布日期 YYYY-mm-dd\n  string last_mtime_text = 4;\n}\n\n// 评论搜索插入项目\nmessage SearchItem {\n  //\n  string url = 1;\n  // 项目\n  oneof item {\n    // 商品\n    GoodsSearchItem goods = 2;\n    // 视频\n    VideoSearchItem video = 3;\n    // 专栏\n    ArticleSearchItem article = 4;\n  }\n}\n\n// 评论搜索插入项目响应游标\nmessage SearchItemCursorReply {\n  // 是否有下一页\n  bool has_next = 1;\n  // 下页\n  int64 next = 2;\n}\n\n// 评论搜索插入项目请求游标\nmessage SearchItemCursorReq {\n  // 下一页\n  int64 next = 1;\n  // tab类型\n  SearchItemType item_type = 2;\n}\n\n// 评论搜索item前置发布-响应\nmessage SearchItemPreHookReply {\n  // 输入框的文案\n  string placeholder_text = 1;\n  // 背景空白的时候的文案\n  string background_text = 2;\n  // 有权限的tab栏的顺序\n  repeated SearchItemType ordered_type = 3;\n}\n\n// 评论搜索item前置发布-请求\nmessage SearchItemPreHookReq {\n  // 目标评论区id\n  int64 oid = 1;\n  // 目标评论区业务type\n  int64 type = 2;\n}\n\n// 评论搜索插入项目-回复\nmessage SearchItemReply {\n  //\n  SearchItemCursorReply cursor = 1;\n  // 搜索的结果\n  repeated SearchItem items = 2;\n  // 附加信息\n  SearchItemReplyExtraInfo extra = 3;\n}\n\n//\nmessage SearchItemReplyExtraInfo {\n  //\n  string event_id = 1;\n}\n\n// 评论搜索插入项目-请求\nmessage SearchItemReq {\n  // 页面游标\n  SearchItemCursorReq cursor = 1;\n  // 目标评论区id\n  int64 oid = 2;\n  // 目标评论区业务type\n  int64 type = 3;\n  // 搜索关键词\n  string keyword = 4;\n}\n\n//\nenum SearchItemType {\n  DEFAULT_ITEM_TYPE = 0; //\n  GOODS = 1; //\n  VIDEO = 2; //\n  ARTICLE = 3; //\n}\n\n//\nenum SearchItemVideoSubType {\n  UGC = 0; //\n  PGC = 1; //\n}\n\n// 评论分享材料-请求\nmessage ShareRepliesInfoReq {\n  // 评论rpid列表\n  repeated int64 rpids = 1;\n  // 目标评论区id\n  int64 oid = 2;\n  // 目标评论区业务type\n  int64 type = 3;\n}\n\n// 评论分享材料-响应\nmessage ShareRepliesInfoResp {\n  //\n  message ShareExtra {\n    //\n    bool is_pgc = 1;\n  }\n  // 评论分享条目列表\n  repeated ShareReplyInfo infos = 1;\n  // 源内容标题\n  string from_title = 2;\n  // 源内容UP主\n  string from_up = 3;\n  // 源内容封面url\n  string from_pic = 4;\n  // 源内容页面url\n  string url = 5;\n  // logo url\n  string slogan_pic = 6;\n  // 标语\n  string slogan_text = 7;\n  //\n  ShareReplyTopic topic = 8;\n  //\n  ShareExtra extra = 9;\n}\n\n// 评论分享条目信息\nmessage ShareReplyInfo {\n  // 用户信息\n  Member member = 1;\n  // 评论主体信息\n  Content content = 2;\n  // 分享标题(评论发布者昵称)\n  string title = 3;\n  // 分享副标题 \"发表了评论\"\n  string sub_title = 4;\n  // 荣誉信息文案 \"获得了up主点赞\"\n  string achievement_text = 5;\n  //\n  string label_url = 6;\n}\n\n//\nmessage ShareReplyTopic {\n  //\n  Topic topic = 1;\n  //\n  string origin_text = 2;\n}\n\n// 评论区控制字段\nmessage SubjectControl {\n  // 评论区筛选类型\n  message FilterTag {\n    // 类型名\n    string name = 1;\n    //\n    string event_id = 2;\n  }\n  // UP主mid\n  int64 up_mid = 1;\n  // 自己是否为协管\n  bool is_assist = 2;\n  // 是否只读\n  bool read_only = 3;\n  // 是否有发起投票权限\n  bool has_vote_access = 4;\n  // 是否有发起抽奖权限\n  bool has_lottery_access = 5;\n  // 是否有被折叠评论\n  bool has_folded_reply = 6;\n  // 空评论区背景文案\n  string bg_text = 7;\n  // 是否被UP拉黑\n  bool up_blocked = 8;\n  // 是否有发起活动权限\n  bool has_activity_access = 9;\n  // 标题展示控制\n  bool show_title = 10;\n  // 是否显示UP主操作标志\n  bool show_up_action = 11;\n  // 是否显示评论区排序切换按钮\n  int64 switcher_type = 12;\n  // 是否禁止输入框\n  bool input_disable = 13;\n  // 根评论输入框背景文案\n  string root_text = 14;\n  // 子评论输入框背景文案\n  string child_text = 15;\n  // 评论总数\n  int64 count = 16;\n  // 评论区标题\n  string title = 17;\n  // 离开态输入框的文案\n  string giveup_text = 18;\n  // 是否允许笔记\n  bool has_note_access = 19;\n  //\n  bool disable_jump_emote = 20;\n  //\n  string empty_background_text_plain = 21;\n  //\n  string empty_background_text_highlight = 22;\n  //\n  string empty_background_uri = 23;\n  // 评论区筛选类型列表\n  repeated FilterTag support_filter_tags = 24;\n}\n\n// 评论表情推荐列表-请求\nmessage SuggestEmotesReq {\n  // 目标评论区id\n  int64 oid = 1;\n  // 目标评论区业务type\n  int64 type = 2;\n}\n\n// 评论表情推荐列表-响应\nmessage SuggestEmotesResp {\n  // 表情推荐列表\n  repeated Emote emotes = 1;\n}\n\n// 话题项\nmessage Topic {\n  // 跳转url\n  string link = 1;\n  // 话题id\n  int64 id = 2;\n}\n\n// UGC视频项目\nmessage UGCVideoSearchItem {\n  // 标题\n  string title = 1;\n  // UP主昵称\n  string up_nickname = 2;\n  // 时长(单位为秒)\n  int64 duration = 3;\n  // 封面\n  string cover = 4;\n}\n\n// 精选评论\nmessage UpSelection {\n  // 待审评论数\n  int64 pending_count = 1;\n  // 忽略评论数\n  int64 ignore_count = 2;\n}\n\n// 超链项\nmessage Url {\n  // 扩展字段\n  message Extra {\n    //\n    int64 goods_item_id = 1;\n    //\n    string goods_prefetched_cache = 2;\n    //\n    int32 goods_show_type = 3;\n    // 热词搜索\n    bool is_word_search = 4;\n    //\n    int64 goods_cm_control = 5;\n  }\n  // 标题\n  string title = 1;\n  //\n  int64 state = 2;\n  // 图标url\n  string prefix_icon = 3;\n  // 客户端内跳转uri\n  string app_url_schema = 4;\n  //\n  string app_name = 5;\n  //\n  string app_package_name = 6;\n  // 点击上报数据\n  string click_report = 7;\n  // 是否半屏打开\n  bool is_half_screen = 8;\n  // 展现上报数据\n  string exposure_report = 9;\n  // 扩展字段\n  Extra extra = 10;\n  // 是否下划线\n  bool underline = 11;\n  //\n  bool match_once = 12;\n  // 网页url\n  string pc_url = 13;\n  //\n  int32 icon_position = 14;\n}\n\n//\nenum UserCallbackAction {\n  Dismiss = 0; //\n}\n\n// 用户回调上报-响应\nmessage UserCallbackReply {}\n\n// 用户回调上报-请求\nmessage UserCallbackReq {\n  // 用户mid\n  int64 mid = 1;\n  //\n  UserCallbackScene scene = 2;\n  //\n  UserCallbackAction action = 3;\n  // 目标评论区id\n  int64 oid = 4;\n  // 目标评论区业务type\n  int64 type = 5;\n}\n\n//\nenum UserCallbackScene {\n  Insert = 0; //\n}\n\n// 视频项目\nmessage VideoSearchItem {\n  //\n  SearchItemVideoSubType type = 1;\n  //\n  oneof video_item {\n    // UGC视频\n    UGCVideoSearchItem ugc = 2;\n    // PGC视频\n    PGCVideoSearchItem pgc = 3;\n  }\n}\n\n// 投票信息\nmessage Vote {\n  // 投票id\n  int64 id = 1;\n  // 投票标题\n  string title = 2;\n  // 参与人数\n  int64 count = 3;\n}\n\n//\nmessage VoteCard{\n  // 投票id\n  int64 vote_id = 1;\n  // 投票标题\n  string title = 2;\n  //\n  int64 count = 3;\n  //\n  repeated VoteCardOption options = 4;\n  //\n  int64 my_vote_option = 5;\n}\n\n//\nmessage VoteCardOption{\n  //\n  int64 idx = 1;\n  //\n  string desc = 2;\n  //\n  int64 count = 3;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/metadata/device/device.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.metadata.device;\n\noption java_multiple_files = true;\n\n// 设备信息\n// gRPC头部:x-bili-device-bin\nmessage Device {\n  // 产品id\n  // 粉 白 蓝 直播姬 HD 海外 OTT 漫画 TV野版 小视频 网易漫画 网易漫画 网易漫画HD 国际版 东南亚版\n  // 1  2  3    4    5   6    7   8     9     10      11       12       13       14       30\n  int32 app_id = 1;\n  // 构建id\n  int32 build = 2;\n  // 设备buvid\n  string buvid = 3;\n  // 包类型\n  string mobi_app = 4;\n  // 平台类型\n  // ios android\n  string platform = 5;\n  // 设备类型\n  string device = 6;\n  // 渠道\n  string channel = 7;\n  // 手机品牌\n  string brand = 8;\n  // 手机型号\n  string model = 9;\n  // 系统版本\n  string osver = 10;\n  // 本地设备指纹\n  string fp_local = 11;\n  // 远程设备指纹\n  string fp_remote = 12;\n  // APP版本号\n  string version_name = 13;\n  // 设备指纹, 不区分本地或远程设备指纹，作为推送目标的索引\n  string fp = 14;\n  // 首次启动时的毫秒时间戳\n  int64 fts = 15;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/metadata/fawkes/fawkes.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.metadata.fawkes;\n\noption java_multiple_files = true;\n\n//\nmessage FawkesReply {\n  // 客户端在fawkes系统中对应的已发布最新的config版本号\n  string config = 1;\n  // 客户端在fawkes系统中对应的已发布最新的ff版本号\n  string ff = 2;\n}\n\n//\nmessage FawkesReq {\n  // 客户端在fawkes系统的唯一名, 如 `android64`\n  string appkey = 1;\n  // 客户端在fawkes系统中的环境参数, 如 `prod`\n  string env = 2;\n  // 启动id, 8 位 0~9, a~z 组成的字符串\n  string session_id = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/metadata/locale/locale.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.metadata.locale;\n\noption java_multiple_files = true;\n\n// 区域标识\n// gRPC头部:x-bili-locale-bin\nmessage Locale {\n  // App设置的locale\n  LocaleIds c_locale = 1;\n  // 系统默认的locale\n  LocaleIds s_locale = 2;\n  // sim卡的国家码+运营商码\n  string sim_code = 3;\n  // 时区\n  string timezone = 4;\n}\n\n// Defined by https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/LanguageandLocaleIDs/LanguageandLocaleIDs.html\nmessage LocaleIds {\n  // A language designator is a code that represents a language.\n  string language = 1;\n  // Writing systems.\n  string script = 2;\n  // A region designator is a code that represents a country or an area.\n  string region = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/metadata/metadata.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.metadata;\n\noption java_multiple_files = true;\n\n// 请求元数据\n// gRPC头部:x-bili-metadata-bin\nmessage Metadata {\n  // 登录 access_key\n  string access_key = 1;\n  // 包类型, 如 `android`\n  string mobi_app = 2;\n  // 运行设备, 留空即可\n  string device = 3;\n  // 构建id, 如 `7380300`\n  int32 build = 4;\n  // APP分发渠道, 如 `master`\n  string channel = 5;\n  // 设备唯一标识\n  string buvid = 6;\n  // 平台类型, 如 `android`\n  string platform = 7;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/metadata/network/network.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.metadata.network;\n\noption java_multiple_files = true;\n\n// 网络类型标识\n// gRPC头部:x-bili-network-bin\nmessage Network {\n  // 网络类型\n  NetworkType type = 1;\n  // 免流类型\n  TFType tf = 2;\n  // 运营商\n  string oid = 3;\n}\n\n// 网络类型\nenum NetworkType {\n  NT_UNKNOWN = 0; // 未知\n  WIFI = 1; // WIFI\n  CELLULAR = 2; // 蜂窝网络\n  OFFLINE = 3; // 未连接\n  OTHERNET = 4; // 其他网络\n  ETHERNET = 5; // 以太网\n}\n\n// 免流类型\nenum TFType {\n  TF_UNKNOWN = 0; // 正常计费\n  U_CARD = 1; // 联通卡\n  U_PKG = 2; // 联通包\n  C_CARD = 3; // 移动卡\n  C_PKG = 4; // 移动包\n  T_CARD = 5; // 电信卡\n  T_PKG = 6; // 电信包\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/metadata/parabox/parabox.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.metadata.parabox;\n\noption java_multiple_files = true;\n\n//\nmessage Exp {\n  //\n  int64 id = 1;\n  //\n  int32 bucket = 2;\n}\n\n//\nmessage Exps {\n  //\n  repeated Exp exps = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/metadata/restriction/restriction.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.metadata.restriction;\n\noption java_multiple_files = true;\n\n// 模式类型\nenum ModeType {\n  NORMAL = 0; // 正常模式\n  TEENAGERS = 1; // 青少年模式\n  LESSONS = 2; // 课堂模式\n}\n\n// 限制条件\nmessage Restriction {\n  // 青少年模式开关状态\n  bool teenagers_mode = 1;\n  // 课堂模式开关状态\n  bool lessons_mode = 2;\n  // 模式类型(旧版)\n  ModeType mode = 3;\n  // app 审核review状态\n  bool review = 4;\n  // 客户端是否选择关闭个性化推荐\n  bool disable_rcmd = 5;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/pagination/pagination.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.pagination;\n\noption java_multiple_files = true;\n\n// 分页信息\nmessage FeedPagination {\n  //\n  int32 page_size = 1;\n  //\n  string offset = 2;\n  //\n  bool is_refresh = 3;\n}\n\n// 分页信息\nmessage FeedPaginationReply {\n  //\n  string next_offset = 1;\n  //\n  string prev_offset = 2;\n  //\n  string last_read_offset = 3;\n}\n\n// 分页信息\nmessage Pagination {\n  //\n  int32 page_size = 1;\n  //\n  string next = 2;\n}\n\n// 分页信息\nmessage PaginationReply {\n  //\n  string next = 1;\n  //\n  string prev = 2;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/pangu/gallery/v1/gallery.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.pangu.gallery.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n//\nservice GalleryInterface {\n  //\n  rpc Ping (google.protobuf.Empty) returns (google.protobuf.Empty);\n  //\n  rpc UserInfo (GetUserInfoReq) returns (GetUserInfoReply);\n  //\n  rpc ListNFTByMid (ListNFTByMidReq) returns (ListNFTByMidReply);\n  //\n  rpc ListOrderByMid (ListOrderByMidReq) returns (ListOrderByMidReply);\n  //\n  rpc BasicInfo (BasicInfoReq) returns (BasicInfoReply);\n  //\n  rpc UserCheck (UserCheckReq) returns (UserCheckReply);\n  //\n  rpc AgreePolicy (AgreePolicyReq) returns (AgreePolicyReply);\n  //\n  rpc GetLastPolicy (GetLastPolicyReq) returns (GetLastPolicyReply);\n}\n\n//\nmessage AgreePolicyReply {\n\n}\n\n//\nmessage AgreePolicyReq {\n  //\n  PolicyType policy_type = 1;\n  //\n  string version = 2;\n}\n\n//\nmessage BasicInfoReply {\n  //\n  string customer_service_url = 1;\n  //\n  string agreement_url = 2;\n  //\n  string privacy_url = 3;\n  //\n  repeated Link links = 4;\n}\n\n//\nmessage BasicInfoReq {\n  //\n  int64 mid = 1;\n}\n\n//\nmessage Display {\n  //\n  string bg_theme_light = 1;\n  //\n  string bg_theme_night = 2;\n  //\n  string nft_poster = 3;\n  //\n  string nft_raw = 4;\n}\n\n//\nenum GT14Status {\n  LT14 = 0;         //\n  GE14 = 1;         //\n  UNKNOWN_GT14 = 2; //\n}\n\n//\nmessage GetLastPolicyReply {\n  //\n  string short_desc = 1;\n  //\n  string detail_jump = 2;\n  //\n  string version = 3;\n}\n\n//\nmessage GetLastPolicyReq {\n  //\n  PolicyType policy_type = 1;\n}\n\n//\nmessage GetUserInfoReply {\n  //\n  int64 mid = 1;\n  //\n  string name = 2;\n  //\n  string address = 3;\n  //\n  string avatar_url = 4;\n  //\n  string help_url = 5;\n}\n\n//\nmessage GetUserInfoReq {\n  //\n  int64 mid = 1;\n}\n\n//\nmessage Link {\n  //\n  string name = 1;\n  //\n  string link_url = 2;\n  //\n  string track_event_id = 3;\n}\n\n//\nmessage ListNFTByMidReply {\n  //\n  repeated NFT nfts = 1;\n  //\n  int64 anchor_id = 2;\n  //\n  bool end = 3;\n}\n\n//\nmessage ListNFTByMidReq {\n  //\n  int64 mid = 1;\n  //\n  string category = 2;\n  //\n  string biz_type = 3;\n  //\n  int64 anchor_id = 4;\n  //\n  int64 page_size = 5;\n}\n\n//\nmessage ListOrderByMidReply {\n  //\n  repeated Order orders = 1;\n  //\n  int64 anchor_id = 2;\n  //\n  bool end = 3;\n}\n\n//\nmessage ListOrderByMidReq {\n  //\n  int64 mid = 1;\n  //\n  int64 anchor_id = 2;\n  //\n  int64 page_size = 3;\n}\n\n//\nmessage NFT {\n  //\n  string nft_id = 1;\n  //\n  string item_name = 2;\n  //\n  string serial_number = 3;\n  //\n  string issuer = 4;\n  //\n  Display display = 5;\n  //\n  string detail_url = 6;\n  //\n  NFTStatus nft_status = 7;\n  //\n  int64 item_id = 8;\n}\n\n//\nenum NFTStatus {\n  UNDEFINED = 0; //\n  NORMAL = 1;    //\n  DOING = 2;     //\n}\n\n//\nmessage Order {\n  //\n  string item_name = 1;\n  //\n  string serial_number = 2;\n  //\n  string tx_hash = 3;\n  //\n  string tx_time = 4;\n  //\n  string issuer = 5;\n  //\n  string issue_time = 6;\n  //\n  string token_id = 7;\n  //\n  Display display = 8;\n  //\n  string contract_address = 9;\n  //\n  string hash_jump = 10;\n  //\n  string contract_jump = 11;\n  //\n  bool disable_browser_jump = 12;\n}\n\n//\nenum PolicyAgreeStatus {\n  UNSIGNED = 0; //\n  ACCEPTED = 1; //\n  EXPIRED = 2;  //\n}\n\n//\nenum PolicyType {\n  UNKNOWN_POLICY = 0; //\n  WALLET = 1;         //\n  SALE = 2;           //\n}\n\n//\nmessage UserCheckReply {\n  //\n  int32 policy_status = 1;\n  //\n  int32 gt14 = 2;\n}\n\n//\nmessage UserCheckReq {\n  //\n  int64 mid = 1;\n  //\n  int32 policy_type = 2;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/pgc/gateway/player/v1/playurl.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.pgc.gateway.player.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/playurl/v1/playurl.proto\";\n\n// 播放地址\nservice PlayURL {\n  // 播放页信息\n  rpc PlayView (PlayViewReq) returns (PlayViewReply);\n  // 获取投屏地址\n  rpc Project (ProjectReq) returns (ProjectReply);\n  // 直播播放页信息\n  rpc LivePlayView (LivePlayViewReq) returns (LivePlayViewReply);\n}\n\n// 其他业务信息\nmessage BusinessInfo {\n  // 当前视频是否是预览\n  bool is_preview = 1;\n  // 用户是否承包过\n  bool bp = 2;\n  // drm使用\n  string marlin_token = 3;\n}\n\n// 事件\nmessage Event {\n  // 震动\n  Shake shake = 1;\n}\n\n// 播放信息\nmessage LivePlayInfo {\n  //\n  int32 current_qn = 1;\n  //\n  repeated QualityDescription quality_description = 2;\n  //\n  repeated ResponseDataUrl durl = 3;\n}\n\n// 直播播放页信息-响应\nmessage LivePlayViewReply {\n  // 房间信息\n  RoomInfo room_info = 1;\n  // 播放信息\n  LivePlayInfo play_info = 2;\n}\n\n// 直播播放页信息-请求\nmessage LivePlayViewReq {\n  // 剧集epid\n  int64 ep_id = 1;\n  // 清晰度\n  // 0,10000:原画 400:蓝光 250:超清 150:高清 80:流畅\n  uint32 quality = 2;\n  // 类型\n  // 0:音频 2:hevc 4:dash 8:p2p, 16:蒙版\n  uint32 ptype = 3;\n  // 是否请求https\n  bool https = 4;\n  // 0:默认直播间播放 1:投屏播放\n  uint32 play_type = 5;\n  // 投屏设备\n  // 0:默认其他 1:OTT设备\n  int32 device_type = 6;\n}\n\n// 禁用功能配置\nmessage PlayAbilityConf {\n  bool background_play_disable = 1;   // 后台播放\n  bool flip_disable = 2;              // 镜像反转\n  bool cast_disable = 3;              // 投屏\n  bool feedback_disable = 4;          // 反馈\n  bool subtitle_disable = 5;          // 字幕\n  bool playback_rate_disable = 6;     // 播放速度\n  bool time_up_disable = 7;           // 定时停止\n  bool playback_mode_disable = 8;     // 播放方式\n  bool scale_mode_disable = 9;        // 画面尺寸\n  bool like_disable = 10;             // 赞\n  bool dislike_disable = 11;          // 踩\n  bool coin_disable = 12;             // 投币\n  bool elec_disable = 13;             // 充电\n  bool share_disable = 14;            // 分享\n  bool screen_shot_disable = 15;      // 截图\n  bool lock_screen_disable = 16;      // 锁定\n  bool recommend_disable = 17;        // 相关推荐\n  bool playback_speed_disable = 18;   // 播放速度\n  bool definition_disable = 19;       // 清晰度\n  bool selections_disable = 20;       // 选集\n  bool next_disable = 21;             // 下一集\n  bool edit_dm_disable = 22;          // 编辑弹幕\n  bool small_window_disable = 23;     // 小窗\n  bool shake_disable = 24;            // 震动\n  bool outer_dm_disable = 25;         // 外层面板弹幕设置\n  bool inner_dm_disable = 26;         // 三点内弹幕设置\n  bool freya_enter_disable = 27;      // 一起看入口\n  bool dolby_disable = 28;            // 杜比音效\n  bool freya_full_disable = 29;       // 全屏一起看入口\n  bool skip_oped_switch_disable = 30; //\n}\n\n//  播放页信息-响应\nmessage PlayViewReply {\n  // 视频流信息\n  bilibili.app.playurl.v1.VideoInfo video_info = 1;\n  // 播放控件用户自定义配置\n  PlayAbilityConf play_conf = 2;\n  // 业务需要的其他信息\n  BusinessInfo business = 3;\n  // 事件\n  Event event = 4;\n}\n\n// 播放页信息-请求\nmessage PlayViewReq {\n  // 剧集epid\n  int64 epid = 1;\n  // 视频cid\n  int64 cid = 2;\n  // 清晰度\n  int64 qn = 3;\n  // 视频流版本\n  int32 fnver = 4;\n  // 视频流格式\n  int32 fnval = 5;\n  // 下载模式\n  // 0:播放 1:flv下载 2:dash下载\n  uint32 download = 6;\n  // 流url强制是用域名\n  // 0:允许使用ip 1:使用http 2:使用https\n  int32 force_host = 7;\n  // 是否4K\n  bool fourk = 8;\n  // 当前页spm\n  string spmid = 9;\n  // 上一页spm\n  string from_spmid = 10;\n  // 青少年模式\n  int32 teenagers_mode = 11;\n  // 视频编码\n  bilibili.app.playurl.v1.CodeType prefer_codec_type = 12;\n  // 是否强制请求预览视频\n  bool is_preview = 13;\n  // 一起看房间id\n  int64 room_id = 14;\n}\n\n// 投屏地址-响应\nmessage ProjectReply {\n  bilibili.app.playurl.v1.PlayURLReply project = 1;\n}\n\n// 投屏地址-请求\nmessage ProjectReq {\n  // 剧集epid\n  int64 ep_id = 1;\n  // 视频cid\n  int64 cid = 2;\n  // 清晰度\n  int64 qn = 3;\n  // 视频流版本\n  int32 fnver = 4;\n  // 视频流格式\n  int32 fnval = 5;\n  // 下载模式\n  // 0:播放 1:flv下载 2:dash下载\n  uint32 download = 6;\n  // 流url强制是用域名\n  // 0:允许使用ip 1:使用http 2:使用https\n  int32 forceHost = 7;\n  // 是否4K\n  bool fourk = 8;\n  // 当前页spm\n  string spmid = 9;\n  // 上一页spm\n  string fromSpmid = 10;\n  // 使用协议\n  // 0:默认乐播 1:自建协议 2:云投屏 3:airplay\n  int32 protocol = 11;\n  // 投屏设备\n  // 0:默认其他 1:OTT设备\n  int32 device_type = 12;\n  //\n  bool use_new_project_code = 13;\n}\n\n//\nmessage QualityDescription {\n  //\n  int32 qn = 1;\n  //\n  string desc = 2;\n}\n\n//\nmessage ResponseDataUrl {\n  string url = 1;\n  // 表示stream类型,按位表示\n  // Value|  1   |  1  |  1   |  1   |     1\n  // --------------------------------------------\n  // desc | mask | p2p | dash | hevc | only-audio\n  uint32 stream_type = 2;\n  // 表示支持p2p的cdn厂商,按位表示\n  // 值   | 1  |  1  |  1  | 1  |  1  | 1  | 1  | 1\n  // -----------------------------------------------\n  // CDN\t| hw | bdy | bsy | ws | txy | qn | js | bvc\n  uint32 ptag = 3;\n}\n\n// 房间信息\nmessage RoomInfo {\n  // 房间长号\n  int64 room_id = 1;\n  // 主播mid\n  int64 uid = 2;\n  // 状态相关\n  RoomStatusInfo status = 3;\n  // 展示相关\n  RoomShowInfo show = 4;\n}\n\n// 房间信息-展示相关\nmessage RoomShowInfo {\n  // 短号\n  int64 short_id = 1;\n  // 人气值\n  int64 popularity_count = 8;\n  // 最近一次开播时间戳\n  int64 live_start_time = 10;\n}\n\n// 房间信息-状态相关\nmessage RoomStatusInfo {\n  // 直播间状态\n  // 0:未开播 1:直播中 2:轮播中\n  int64 live_status = 1;\n  // 横竖屏方向\n  // 0:横屏 1:竖屏\n  int64 live_screen_type = 2;\n  // 是否开播过标识\n  int64 live_mark = 3;\n  // 封禁状态\n  // 0:未封禁 1:审核封禁 2:全网封禁\n  int64 lock_status = 4;\n  // 封禁时间戳\n  int64 lock_time = 5;\n  // 隐藏状态\n  // 0:不隐藏 1:隐藏\n  int64 hidden_status = 6;\n  // 隐藏时间戳\n  int64 hidden_time = 7;\n  // 直播类型\n  // 0:默认 1:摄像头直播 2;录屏直播 3:语音直播\n  int64 live_type = 8;\n  //\n  int64 room_shield = 9;\n}\n\n// 震动\nmessage Shake {\n  // 文件地址\n  string file = 1;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/pgc/gateway/player/v2/playurl.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.pgc.gateway.player.v2;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/timestamp.proto\";\n\n// 视频url\nservice PlayURL {\n  // 播放页信息\n  rpc PlayView (PlayViewReq) returns (PlayViewReply);\n  //\n  rpc PlayViewComic(PlayViewReq) returns (PlayViewReply);\n}\n\n//\nmessage Animation {\n  //\n  map<string, string> qn_svga_animation_map = 1;\n}\n\n//\nmessage AudioMaterialProto {\n  //\n  string audio_id = 1;\n  //\n  string title = 2;\n  //\n  string edition = 3;\n  //\n  uint64 person_id = 4;\n  //\n  string person_name = 5;\n  //\n  string person_avatar = 6;\n  //\n  repeated DashItem audio = 7;\n}\n\n// 角标信息\nmessage BadgeInfo {\n  // 角标文案\n  string text = 1;\n  // 角标色值\n  string bg_color = 2;\n  // 角标色值-夜间模式\n  string bg_color_night = 3;\n  // 文案色值\n  string text_color = 4;\n  // ? 新版本客户端已弃用此项\n  GradientColor bg_gradient_color = 5;\n  //\n  string img = 6;\n}\n\n// Dialog组件: 底部显示\nmessage BottomDisplay {\n  // 文案\n  TextInfo title = 1;\n  // 图标\n  string icon = 2;\n}\n\n// 按钮信息\nmessage ButtonInfo {\n  // 按钮文案\n  string text = 1;\n  // 按钮字体色值\n  string text_color = 2;\n  // 按钮字体色值-夜间模式\n  string text_color_night = 3;\n  // 按钮背景色\n  string bg_color = 4;\n  // 按钮背景色-夜间模式\n  string bg_color_night = 5;\n  // 按钮链接\n  string link = 6;\n  // 按钮动作类型\n  string action_type = 7;\n  // 角标信息\n  BadgeInfo badge_info = 8;\n  // 埋点上报信息\n  Report report = 9;\n  // 左侧删除线样式文案\n  string left_strikethrough_text = 10;\n  // 缩略按钮文案信息\n  TextInfo simple_text_info = 11;\n  // 缩略按钮背景色值\n  string simple_bg_color = 12;\n  // 缩略按钮字体色值-夜间模式\n  string simple_bg_color_night = 13;\n  //\n  GradientColor bg_gradient_color = 14;\n  //\n  map<string, string> order_report_params = 15;\n  //\n  TaskParam task_param = 16;\n  //\n  string pc_link = 17;\n}\n\n// 投屏限制. code = 0 时为无限制, 否则表示不不允许投屏并提示message\nmessage CastTips {\n  //\n  int32 code = 1;\n  //\n  string message = 2;\n}\n\n// 跳过片头/片尾配置\nmessage ClipInfo {\n  //\n  int64 material_no = 1;\n  // DASH分段始\n  int32 start = 2;\n  // DASH分段终\n  int32 end = 3;\n  // Clip类型\n  ClipType clip_type = 4;\n  // 跳过片头/片尾时的提示语\n  string toast_text = 5;\n  //\n  MultiView multi_view = 6;\n}\n\n// 跳过片头/片尾配置: Clip类型\nenum ClipType {\n  NT_UNKNOWN = 0;           //\n  CLIP_TYPE_OP = 1;         // 跳过OP\n  CLIP_TYPE_ED = 2;         // 跳过ED\n  CLIP_TYPE_HE = 3;         //\n  CLIP_TYPE_MULTI_VIEW = 4; //\n  CLIP_TYPE_AD = 5;         //\n}\n\n// 编码类型\nenum CodeType {\n  NOCODE = 0;  // 默认\n  CODE264 = 1; // H.264\n  CODE265 = 2; // H.265\n}\n\n//\nmessage ContinuePlayInfo {\n  //\n  int64 continue_play_ep_id = 1;\n}\n\n// 优惠券\nmessage Coupon {\n  // 优惠券token\n  string coupon_token = 1;\n  // 优惠券类型\n  // 1:折扣券 2:满减券 3:兑换券\n  int64 type = 2;\n  // 优惠券面值\n  string value = 3;\n  // 优惠券使用描述\n  string use_desc = 4;\n  // 优惠券标题\n  string title = 5;\n  // 优惠券描述\n  string desc = 6;\n  // 优惠券支付按钮文案\n  string pay_button_text = 7;\n  // 优惠券支付按钮删除线文案\n  string pay_button_text_line_through = 8;\n  // 实付金额\n  string real_amount = 9;\n  // 使用过期时间\n  google.protobuf.Timestamp expire_time = 10;\n  //\n  int64 otype = 11;\n  //\n  string amount = 12;\n}\n\n// 优惠券信息\nmessage CouponInfo {\n  // 提示框信息\n  CouponToast toast = 1;\n  // 弹窗信息\n  PopWin pop_win = 2;\n}\n\n// 优惠券提示框文案信息\nmessage CouponTextInfo {\n  // 提示框文案-播正片6分钟预览\n  string positive_preview = 1;\n  // 提示框文案-播非正片分节ep\n  string section = 2;\n}\n\n// 优惠券提示框信息\nmessage CouponToast {\n  // 提示框文案信息\n  CouponTextInfo text_info = 1;\n  // 提示框按钮\n  ButtonInfo button = 2;\n}\n\n// dash条目\nmessage DashItem {\n  // 清晰度\n  uint32 id = 1;\n  // 主线流\n  string base_url = 2;\n  // 备用流\n  repeated string backup_url = 3;\n  // 带宽\n  uint32 bandwidth = 4;\n  // 编码id\n  uint32 codecid = 5;\n  // md5\n  string md5 = 6;\n  // 视频大小\n  uint64 size = 7;\n  // 帧率\n  string frame_rate = 8;\n  // DRM widevine 密钥\n  string widevine_pssh = 9;\n}\n\n// dash视频流\nmessage DashVideo {\n  // 主线流\n  string base_url = 1;\n  // 备用流\n  repeated string backup_url = 2;\n  // 带宽\n  uint32 bandwidth = 3;\n  // 编码id\n  uint32 codecid = 4;\n  // md5\n  string md5 = 5;\n  // 大小\n  uint64 size = 6;\n  // 伴音质量id\n  uint32 audio_id = 7;\n  // 是否非全二压\n  bool no_rexcode = 8;\n  // 帧率\n  string frame_rate = 9;\n  // 宽\n  int32 width = 10;\n  // 高\n  int32 height = 11;\n  // DRM 密钥\n  string widevine_pssh = 12;\n}\n\n//\nmessage DataControl {\n  //\n  bool need_watch_progress = 1;\n}\n\n// 鉴权浮层\nmessage Dialog {\n  // 鉴权限制码\n  int64 code = 1;\n  // 鉴权限制信息\n  string msg = 2;\n  // 浮层类型\n  string type = 3;\n  // 浮层样式类型\n  string style_type = 4;\n  // 浮层配置\n  DialogConfig config = 5;\n  // 标题\n  TextInfo title = 6;\n  // 副标题\n  TextInfo subtitle = 7;\n  // 图片信息\n  ImageInfo image = 8;\n  // 按钮列表\n  repeated ButtonInfo button = 9;\n  // 底部描述\n  ButtonInfo bottom_desc = 10;\n  // 埋点上报信息\n  Report report = 11;\n  // 倒计时 秒\n  int32 count_down_sec = 12;\n  // 右下描述\n  TextInfo right_bottom_desc = 13;\n  //\n  repeated BottomDisplay bottom_display = 14;\n  //\n  repeated PlayList play_list = 15;\n}\n\n// 鉴权浮层配置\nmessage DialogConfig {\n  // 是否显示高斯模糊背景图\n  bool is_show_cover = 1;\n  // 是否响应转屏\n  bool is_orientation_enable = 2;\n  // 是否响应上滑吸顶\n  bool is_nested_scroll_enable = 3;\n  // 是否强制竖屏\n  bool is_force_halfscreen_enable = 4;\n  // 是否启用背景半透明\n  bool is_background_translucent_enable = 5;\n}\n\n// 当前分辨率信息\nmessage Dimension {\n  // 宽\n  int32 width = 1;\n  // 长\n  int32 height = 2;\n  // 旋转角度\n  int32 rotate = 3;\n}\n\n// 杜比音频信息\nmessage DolbyItem {\n  // 杜比类型\n  enum Type {\n    NONE = 0;   // NONE\n    COMMON = 1; // 普通杜比音效\n    ATMOS = 2;  // 全景杜比音效\n  }\n  // 杜比类型\n  Type type = 1;\n  // 音频流\n  DashItem audio = 2;\n}\n\n// DRM技术类型\nenum DrmTechType {\n  NON = 0;       //\n  FAIR_PLAY = 1; //\n  WIDE_VINE = 2;  //\n  BILI_DRM = 3; //\n}\n\n// 播放结束后的尾页Dialog\nmessage EndPage {\n  //\n  Dialog dialog = 1;\n  //\n  bool hide = 2;\n}\n\n//\nmessage EpInlineVideo {\n  //\n  int64 material_no = 1;\n  //\n  int64 aid = 2;\n  //\n  int64 cid = 3;\n}\n\n// 剧集广告信息\nmessage EpisodeAdvertisementInfo {\n  //\n  int64 aid = 1;\n  //\n  string title = 2;\n  //\n  string link = 3;\n  //\n  int32 follow_video_bnt_flag = 4;\n  //\n  string next_video_title = 5;\n  //\n  string next_video_link = 6;\n  //\n  int64 cid = 7;\n  //\n  int32 season_id = 8;\n  //\n  int32 follow = 9;\n}\n\n// EP信息\nmessage EpisodeInfo {\n  //\n  int32 ep_id = 1;\n  //\n  int64 cid = 2;\n  //\n  int64 aid = 3;\n  //\n  int64 ep_status = 4;\n  //\n  SeasonInfo season_info = 5;\n  //\n  string cover = 6;\n  //\n  string title = 7;\n  //\n  Interaction interaction = 8;\n  //\n  string long_title = 9;\n}\n\n//\nmessage EpPreVideo {\n  //\n  int64 aid = 1;\n  //\n  int64 cid = 2;\n}\n\n//\nmessage EpPublicityVideo {\n  //\n  enum Type {\n    DATA_NOT_SET = 0;\n    EP_PRE_VIDEO = 2;\n    EP_INLINE = 3;\n  }\n  //\n  Type type = 1;\n  //\n  oneof data {\n    //\n    EpPreVideo ep_pre_video = 2;\n    //\n    EpInlineVideo ep_inline_video = 3;\n  }\n}\n\n//\nenum EpPublicityVideoType {\n  //\n  PRE = 0;\n  //\n  INLINE = 1;\n}\n\n// 事件\nmessage Event {\n  // 震动\n  Shake shake = 1;\n}\n\n// 放映室提示语\nmessage FreyaConfig {\n  //\n  string desc = 1;\n  //\n  int32 type = 2;\n  //\n  int32 issued_cnt = 3;\n  //\n  bool is_always_show = 4;\n  //\n  int32 screen_number = 5;\n  //\n  int32 full_screen_number = 6;\n}\n\n// 渐变色信息\nmessage GradientColor {\n  //\n  string start_color = 1;\n  //\n  string end_color = 2;\n}\n\n// 高画质试看信息\nmessage HighDefinitionTrialInfo {\n  //\n  bool trial_able = 1;\n  //\n  int32 remaining_times = 2;\n  //\n  int32 start = 3;\n  //\n  int32 time_length = 4;\n  //\n  Toast start_toast = 5;\n  //\n  Toast end_toast = 6;\n  //\n  Report report = 7;\n  //\n  ButtonInfo quality_open_tip_btn = 8;\n  //\n  ButtonInfo no_longer_trial_btn = 9;\n}\n\n// 历史记录节点\nmessage HistoryNode {\n  // 节点ID\n  int64 node_id = 1;\n  // 节点标题\n  string title = 2;\n  // 对应CID\n  int64 cid = 3;\n}\n\n// 图片信息\nmessage ImageInfo {\n  // 图片链接\n  string url = 1;\n}\n\n//\nenum InlineScene {\n  UNKNOWN = 0;    //\n  RELATED_EP = 1; //\n  HE = 2;         //\n  SKIP = 3;       //\n}\n\n//\nenum InlineType {\n  TYPE_UNKNOWN = 0; //\n  TYPE_WHOLE = 1;   //\n  TYPE_HE_CLIP = 2; //\n  TYPE_PREVIEW = 3; //\n}\n\n// 交互信息\nmessage Interaction {\n  // 历史节点\n  HistoryNode history_node = 1;\n  // 版本\n  int64 graph_version = 2;\n  // 交互消息\n  string msg = 3;\n  // 是否为交互\n  bool is_interaction = 4;\n}\n\n// 限制操作类型\nenum LimitActionType {\n  //\n  LAT_UNKNOWN = 0;\n  //\n  SHOW_LIMIT_DIALOG = 1;\n  //\n  SKIP_CURRENT_EP = 2;\n}\n\n//\nmessage MultiView {\n  //\n  string button_material = 1;\n  //\n  int64 ep_id = 2;\n  //\n  int64 cid = 3;\n  //\n  int64 avid = 4;\n}\n\n// 大会员广告: 支付提示信息\nmessage PayTip {\n  // 标题\n  string title = 1;\n  // 跳转链接\n  string url = 2;\n  // 图标\n  string icon = 3;\n  // 浮层类型\n  int32 type = 4;\n  // 显示类型\n  int32 show_type = 5;\n  // 图片信息\n  string img = 6;\n  // 白天背景颜色\n  string bg_day_color = 7;\n  // 夜间背景颜色\n  string bg_night_color = 8;\n  // 白天线条颜色\n  string bg_line_color = 9;\n  // 夜间线条颜色\n  string bg_night_line_color = 10;\n  // 文字颜色\n  string text_color = 11;\n  // 夜间文字颜色\n  string text_night_color = 12;\n  // 视图展示起始时间\n  int64 view_start_time = 13;\n  // 按钮列表\n  repeated ButtonInfo button = 14;\n  // 跳转链接打开方式\n  int32 url_open_type = 15;\n  // 埋点上报信息\n  Report report = 16;\n  // 角度样式\n  int32 angle_style = 17;\n  // 埋点上报类型\n  int32 report_type = 18;\n  // 订单埋点上报参数\n  map<string, string> order_report_params = 19;\n  // 巨屏图片信息\n  string giant_screen_img = 20;\n}\n\n// 禁用功能配置\nmessage PlayAbilityConf {\n  bool background_play_disable = 1;   // 后台播放\n  bool flip_disable = 2;              // 镜像反转\n  bool cast_disable = 3;              // 投屏\n  bool feedback_disable = 4;          // 反馈\n  bool subtitle_disable = 5;          // 字幕\n  bool playback_rate_disable = 6;     // 播放速度\n  bool time_up_disable = 7;           // 定时停止\n  bool playback_mode_disable = 8;     // 播放方式\n  bool scale_mode_disable = 9;        // 画面尺寸\n  bool like_disable = 10;             // 赞\n  bool dislike_disable = 11;          // 踩\n  bool coin_disable = 12;             // 投币\n  bool elec_disable = 13;             // 充电\n  bool share_disable = 14;            // 分享\n  bool screen_shot_disable = 15;      // 截图\n  bool lock_screen_disable = 16;      // 锁定\n  bool recommend_disable = 17;        // 相关推荐\n  bool playback_speed_disable = 18;   // 播放速度\n  bool definition_disable = 19;       // 清晰度\n  bool selections_disable = 20;       // 选集\n  bool next_disable = 21;             // 下一集\n  bool edit_dm_disable = 22;          // 编辑弹幕\n  bool small_window_disable = 23;     // 小窗\n  bool shake_disable = 24;            // 震动\n  bool outer_dm_disable = 25;         // 外层面板弹幕设置\n  bool inner_dm_disable = 26;         // 三点内弹幕设置\n  bool freya_enter_disable = 27;      // 一起看入口\n  bool dolby_disable = 28;            // 杜比音效\n  bool freya_full_disable = 29;       // 全屏一起看入口\n  bool skip_oped_switch_disable = 30; // 跳过片头片尾\n  bool record_screen_disable = 31;    // 录屏\n  bool color_optimize_disable = 32;   // 色觉优化\n  bool dubbing_disable = 33;          // 配音\n}\n\n// 云控扩展配置信息\nmessage PlayAbilityExtConf {\n  //\n  bool allow_close_subtitle = 1;\n  //\n  FreyaConfig freya_config = 2;\n  //\n  CastTips cast_tips = 3;\n}\n\n// 播放配音信息\nmessage PlayDubbingInfo {\n  // 背景音频\n  AudioMaterialProto background_audio = 1;\n  // 角色音频列表\n  repeated RoleAudioProto role_audio_list = 2;\n  // 引导文本\n  string guide_text = 3;\n}\n\n// 错误码\nenum PlayErr {\n  NoErr = 0;                   //\n  WithMultiDeviceLoginErr = 1; // 管控类型的错误码\n}\n\n\n// 播放扩展信息\nmessage PlayExtInfo {\n  // 播放配音信息\n  PlayDubbingInfo play_dubbing_info = 1;\n}\n\n//\nmessage PlayList {\n  //\n  int32 season_id = 1;\n  //\n  string title = 2;\n  //\n  string cover = 3;\n  //\n  string link = 4;\n  //\n  BadgeInfo badge_info = 5;\n}\n\n// 其他业务信息\nmessage PlayViewBusinessInfo {\n  // 当前视频是否是预览\n  bool is_preview = 1;\n  // 用户是否承包过\n  bool bp = 2;\n  // drm使用\n  string marlin_token = 3;\n  // 倍速动效色值\n  string playback_speed_color = 4;\n  //\n  ContinuePlayInfo continue_play_info = 5;\n  // 跳过片头/片尾配置\n  repeated ClipInfo clip_info = 6;\n  //\n  InlineType inline_type = 7;\n  //\n  int32 ep_whole_duration = 8;\n  // 当前分辨率信息\n  Dimension dimension = 9;\n  //\n  map<int32, QualityExtInfo> quality_ext_map = 10;\n  //\n  map<string, int32> exp_map = 11;\n  // DRM技术类型\n  DrmTechType drm_tech_type = 12;\n  //\n  int32 limit_action_type = 13;\n  //\n  bool is_drm = 14;\n  //\n  RecordInfo record_info = 15;\n  //\n  int32 vip_status = 16;\n  //\n  bool is_live_pre = 17;\n  //\n  EpisodeInfo episode_info = 18;\n  //\n  EpisodeAdvertisementInfo episode_advertisement_info = 19;\n  //\n  UserStatus user_status = 20;\n}\n\n// 播放页信息-响应\nmessage PlayViewReply {\n  // 视频流信息\n  VideoInfo video_info = 1;\n  // 播放控件用户自定义配置\n  PlayAbilityConf play_conf = 2;\n  // 业务需要的其他信息\n  PlayViewBusinessInfo business = 3;\n  // 事件\n  Event event = 4;\n  // 展示信息\n  ViewInfo view_info = 5;\n  // 自定义配置扩展信息\n  PlayAbilityExtConf play_ext_conf = 6;\n  // 播放扩展信息\n  PlayExtInfo play_ext_info = 7;\n}\n\n// 播放页信息-请求\nmessage PlayViewReq {\n  // 剧集epid\n  int64 epid = 1;\n  // 视频cid\n  int64 cid = 2;\n  // 清晰度\n  int64 qn = 3;\n  // 视频流版本\n  int32 fnver = 4;\n  // 视频流格式\n  int32 fnval = 5;\n  // 下载模式\n  // 0:播放 1:flv下载 2:dash下载\n  uint32 download = 6;\n  // 流url强制是用域名\n  // 0:允许使用ip 1:使用http 2:使用https\n  int32 force_host = 7;\n  // 是否4K\n  bool fourk = 8;\n  // 当前页spm\n  string spmid = 9;\n  // 上一页spm\n  string from_spmid = 10;\n  // 青少年模式\n  int32 teenagers_mode = 11;\n  // 视频编码\n  CodeType prefer_codec_type = 12;\n  // 是否强制请求预览视频\n  bool is_preview = 13;\n  // 一起看房间id\n  int64 room_id = 14;\n  // 是否需要展示信息\n  bool is_need_view_info = 15;\n  // 场景控制\n  SceneControl scene_control = 16;\n  //\n  InlineScene inline_scene = 17;\n  //\n  int64 material_no = 18;\n  // DRM 安全等级\n  int32 security_level = 19;\n  //\n  int64 season_id = 20;\n  //\n  DataControl data_control = 21;\n}\n\n// 弹窗信息\nmessage PopWin {\n  // 弹窗标题 老字段\n  string title = 1;\n  // 优惠券列表\n  repeated Coupon coupon = 2;\n  // 弹窗按钮列表\n  repeated ButtonInfo button = 3;\n  // 底部文案 老字段\n  string bottom_text = 4;\n  // 弹窗标题 新字段\n  TextInfo pop_title = 5;\n  // 弹窗副标题\n  TextInfo subtitle = 6;\n  // 底部描述 新字段\n  ButtonInfo bottom_desc = 7;\n  // 弹窗小图\n  string cover = 8;\n  // 弹窗类型\n  string pop_type = 9;\n}\n\n// 广告组件: 竖屏时视频下部提示栏\nmessage PromptBar {\n  // 主标题, 如: \"本片含大会员专享内容\"\n  TextInfo title = 1;\n  // 副标题, 如: \"成为大会员可免费看全部剧集\"\n  TextInfo sub_title = 2;\n  // 副标题前面的icon\n  string sub_title_icon = 3;\n  // 背景图\n  string bg_image = 4;\n  // 背景渐变色\n  GradientColor bg_gradient_color = 5;\n  // 按钮\n  repeated ButtonInfo button = 6;\n  // 埋点上报信息\n  Report report = 7;\n  //\n  string full_screen_ip_icon = 8;\n  //\n  GradientColor full_screen_bg_gradient_color = 9;\n}\n\n// 云控拓展视频画质信息\nmessage QualityExtInfo {\n  // 是否支持试看\n  bool trial_support = 1;\n}\n\n// 备案信息\nmessage RecordInfo {\n  // 记录\n  string record = 1;\n  // 记录图标\n  string record_icon = 2;\n}\n\n// 埋点上报信息\nmessage Report {\n  // 曝光事件\n  string show_event_id = 1;\n  // 点击事件\n  string click_event_id = 2;\n  // 埋点透传参数\n  string extends = 3;\n}\n\n// 分段流条目\nmessage ResponseUrl {\n  // 分段序号\n  uint32 order = 1;\n  // 分段时长\n  uint64 length = 2;\n  // 分段大小\n  uint64 size = 3;\n  // 主线流\n  string url = 4;\n  // 备用流\n  repeated string backup_url = 5;\n  // md5\n  string md5 = 6;\n}\n\n// 权限信息\nmessage Rights {\n  // 是否可以观看\n  int32 can_watch = 1;\n}\n\n// 角色配音信息\nmessage RoleAudioProto {\n  // 角色ID\n  int64 role_id = 1;\n  // 角色名称\n  string role_name = 2;\n  // 角色头像\n  string role_avatar = 3;\n  // 音频素材列表\n  repeated AudioMaterialProto audio_material_list = 4;\n}\n\n// 场景控制\nmessage SceneControl {\n  // 是否收藏播单\n  bool fav_playlist = 1;\n  // 是否小窗\n  bool small_window = 2;\n  // 是否画中画\n  bool pip = 3;\n  //\n  bool was_he_inline = 4;\n  //\n  bool is_need_trial = 5;\n}\n\n// 方案\nmessage Scheme {\n  enum ActionType {\n    UNKNOWN = 0;\n    SHOW_TOAST = 1;\n  }\n  //\n  ActionType action_type = 1;\n  //\n  string toast = 2;\n}\n\n// PGC SEASON 信息\nmessage SeasonInfo {\n  // PGC SEASON ID\n  int32 season_id = 1;\n  // PGC SEASON 类型\n  int32 season_type = 2;\n  // PGC SEASON 状态\n  int32 season_status = 3;\n  // 封面\n  string cover = 4;\n  // 标题\n  string title = 5;\n  // 权限信息\n  Rights rights = 6;\n  // 模式\n  int32 mode = 7;\n}\n\n// DRM 安全等级\nenum SecurityLevel {\n  LEVEL_UNKNOWN = 0; //\n  LEVEL_L1 = 1; //\n  LEVEL_L2 = 2; //\n  LEVEL_L3 = 3; //\n}\n\n// 分段视频流\nmessage SegmentVideo {\n  //分段视频流列表\n  repeated ResponseUrl segment = 1;\n}\n\n// 震动\nmessage Shake {\n  // 文件地址\n  string file = 1;\n}\n\n// 视频流信息\nmessage Stream {\n  // 元数据\n  StreamInfo info = 1;\n  // 流数据\n  oneof content {\n    // dash流\n    DashVideo dash_video = 2;\n    // 分段流\n    SegmentVideo segment_video = 3;\n  }\n}\n\n// 流媒体元数据\nmessage StreamInfo {\n  // 视频质量\n  int32 quality = 1;\n  // 视频格式\n  string format = 2;\n  // 描述信息\n  string description = 3;\n  // 错误码\n  int32 err_code = 4;\n  // 流限制信息\n  StreamLimit limit = 5;\n  // 是否需要VIP\n  bool need_vip = 6;\n  // 是否需要登录\n  bool need_login = 7;\n  // 是否完整\n  bool intact = 8;\n  // 权限信息\n  int64 attribute = 10;\n  // 新版描述信息\n  string new_description = 11;\n  // 显示描述信息\n  string display_desc = 12;\n  // 上标\n  string superscript = 13;\n  // 方案信息\n  Scheme scheme = 14;\n  // 是否支持DRM\n  bool support_drm = 15;\n  // 字幕信息\n  string subtitle = 16;\n}\n\n// 清晰度不满足条件信息\nmessage StreamLimit {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 提示信息\n  string msg = 3;\n}\n\n// 任务参数信息\nmessage TaskParam {\n  // 任务类型\n  string task_type = 1;\n  // 活动ID\n  int64 activity_id = 2;\n  // 提示ID\n  int64 tips_id = 3;\n}\n\n// 文案信息\nmessage TextInfo {\n  // 文案\n  string text = 1;\n  // 字体色值\n  string text_color = 2;\n  // 字体色值-夜间模式\n  string text_color_night = 3;\n}\n\n// toast\nmessage Toast {\n  // toast文案 老字段\n  string text = 1;\n  // toast按钮\n  ButtonInfo button = 2;\n  // 显示样式类型\n  int32 show_style_type = 3;\n  // 图标\n  string icon = 4;\n  // toast文案 新字段\n  TextInfo toast_text = 5;\n  // 埋点上报信息\n  Report report = 6;\n  //\n  map<string, string> order_report_params = 7;\n}\n\n// 用户状态信息\nmessage UserStatus {\n  // 是否支付\n  bool pay_check = 1;\n  // 是否承包\n  bool sponsor = 2;\n  // 观看进度\n  WatchProgress watch_progress = 3;\n  // 系列观看进度\n  WatchProgress aid_watch_progress = 4;\n}\n\n// 视频url信息\nmessage VideoInfo {\n  // 视频清晰度\n  uint32 quality = 1;\n  // 视频格式\n  string format = 2;\n  // 视频时长\n  uint64 timelength = 3;\n  // 视频编码id\n  uint32 video_codecid = 4;\n  // 视频流\n  repeated Stream stream_list = 5;\n  // 伴音流\n  repeated DashItem dash_audio = 6;\n  // 杜比伴音流\n  DolbyItem dolby = 7;\n}\n\n// 展示信息\nmessage ViewInfo {\n  // 弹窗\n  Dialog dialog = 1;\n  // Toast\n  Toast toast = 2;\n  // 优惠券信息\n  CouponInfo coupon_info = 3;\n  // 未支付剧集ID列表\n  repeated int64 demand_no_pay_epids = 4;\n  // 结束页\n  EndPage end_page = 5;\n  // 扩展配置\n  map<string, bool> exp_config = 6;\n  // 弹窗\n  PopWin pop_win = 7;\n  // 试看提示栏\n  PromptBar try_watch_prompt_bar = 8;\n  // 支付提示信息\n  PayTip pay_tip = 9;\n  // 高清试看提示信息\n  HighDefinitionTrialInfo high_definition_trial_info = 10;\n  // 弹窗扩展\n  map<string, Dialog> ext_dialog = 11;\n  // 动画\n  Animation animation = 12;\n  // Toast扩展\n  map<string, Toast> ext_toast = 13;\n}\n\n// 观看进度信息\nmessage WatchProgress {\n  // 上次观看的 EP ID\n  int32 last_ep_id = 1;\n  // 上次观看到的EP INDEX\n  string last_ep_index = 2;\n  // 上次观看的进度\n  int64 progress = 3;\n  // 上次观看的 CID\n  int64 last_play_cid = 4;\n  // 带时间的提示信息\n  Toast toast = 5;\n  // 不带时间的提示信息\n  Toast toast_without_time = 6;\n  // 上次观看的 AID\n  int64 last_play_aid = 7;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/pgc/service/premiere/v1/premiere.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.pgc.service.premiere.v1;\n\noption java_multiple_files = true;\n\n// 首播服务\nservice Premiere {\n  // 获取首播状态\n  rpc Status (PremiereStatusReq) returns (PremiereStatusReply);\n}\n\n// 获取首播状态-请求\nmessage PremiereStatusReq {\n  // 剧集epid\n  int64 ep_id = 1;\n}\n\n// 获取首播状态-响应\nmessage PremiereStatusReply {\n  // 服务端播放进度 单位ms 用户实际播放进度：progress - delay_time\n  int64 progress = 1;\n  // 起播时间戳 单位ms\n  int64 start_time = 2;\n  // 延迟播放时长 单位ms\n  int64 delay_time = 3;\n  // 首播在线人数\n  int64 online_count = 4;\n  // 首播状态\n  // 1:预热 2:首播中 3:紧急停播 4:已结束\n  int32 status = 5;\n  // 首播结束后跳转类型\n  // 1:下架 2:转点播\n  int32 after_premiere_type = 6;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/playershared/playershared.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.playershared;\n\noption java_multiple_files = true;\n\n// ArcConf消息\nmessage ArcConf {\n  // 是否支持\n  bool is_support = 1;\n  // 是否禁用\n  bool disabled = 2;\n  // 额外内容\n  ExtraContent extra_content = 3;\n  // 不支持场景列表\n  repeated int32 unsupport_scene = 4;\n}\n\n//\nenum ArcType {\n  //\n  ARC_TYPE_NORMAL = 0;\n  //\n  ARC_TYPE_INTERACT = 1;\n}\n\n//\nmessage BackgroundInfo {\n  //\n  string drawable_color = 1;\n  //\n  string drawable_bitmap_url = 2;\n  //\n  int32 effects = 3;\n}\n\n//\nmessage BadgeInfo {\n  //\n  string text = 1;\n  //\n  string bg_color = 2;\n  //\n  string bg_color_night = 3;\n  //\n  string text_color = 4;\n  //\n  GradientColor bg_gradient_color = 5;\n  //\n  string img = 6;\n}\n\n//\nenum BizType {\n  //\n  BIZ_TYPE_UNKNOWN = 0;\n  //\n  BIZ_TYPE_UGC = 1;\n  //\n  BIZ_TYPE_PGC = 2;\n  //\n  BIZ_TYPE_PUGV = 3;\n}\n\n//\nmessage BottomDisplay {\n  //\n  TextInfo title = 1;\n  //\n  string icon = 2;\n}\n\n// 按钮组件\nmessage Button {\n  // 按钮文本\n  string text = 1;\n  // 按钮跳转链接\n  string link = 2;\n  // 埋点上报相关\n  map<string, string> report_params = 3;\n}\n\nenum ButtonAction {\n  //\n  BUTTON_UNKNOWN = 0;\n  //\n  PAY = 1;\n  //\n  VIP = 2;\n  //\n  PACK = 3;\n  //\n  LINK = 4;\n  //\n  COUPON = 5;\n  //\n  DEMAND = 6;\n  //\n  DEMAND_PACK = 7;\n  //\n  FOLLOW = 8;\n  //\n  APPOINTMENT = 9;\n  //\n  VIP_FREE = 10;\n  //\n  TASK = 11;\n  //\n  CHARGINGPLUS = 12;\n  //\n  BP = 13;\n  //\n  PRE_SELL = 14;\n}\n\n//\nmessage ButtonInfo {\n  //\n  string text = 1;\n  //\n  string text_color = 2;\n  //\n  string text_color_night = 3;\n  //\n  string bg_color = 4;\n  //\n  string bg_color_night = 5;\n  //\n  string link = 6;\n  //\n  ButtonAction action_type = 7;\n  //\n  BadgeInfo badge_info = 8;\n  //\n  Report report = 9;\n  //\n  string left_strikethrough_text = 10;\n  //\n  TextInfo simple_text_info = 11;\n  //\n  string simple_bg_color = 12;\n  //\n  string simple_bg_color_night = 13;\n  //\n  GradientColor bg_gradient_color = 14;\n  //\n  map<string, string> order_report_params = 15;\n  //\n  TaskParam task_param = 16;\n  //\n  string frame_color = 17;\n  //\n  string icon = 18;\n}\n\n// 视频编码\nenum CodeType {\n  NOCODE = 0; // 不指定\n  CODE264 = 1; // H264\n  CODE265 = 2; // H265\n  CODEAV1 = 3; // AV1\n}\n\n//\nmessage ComprehensiveToast {\n  //\n  int32 type = 1;\n  //\n  ButtonInfo button = 2;\n  //\n  int32 show_style_type = 3;\n  //\n  string icon = 4;\n  //\n  TextInfo toast_text = 5;\n  //\n  Report report = 6;\n  //\n  map<string, string> order_report_params = 7;\n}\n\n// 功能类型\nenum ConfType {\n  NoType = 0;\n  FLIPCONF = 1;\n  CASTCONF = 2;\n  FEEDBACK = 3;\n  SUBTITLE = 4;\n  PLAYBACKRATE = 5;\n  TIMEUP = 6;\n  PLAYBACKMODE = 7;\n  SCALEMODE = 8;\n  BACKGROUNDPLAY = 9;\n  LIKE = 10;\n  DISLIKE = 11;\n  COIN = 12;\n  ELEC = 13;\n  SHARE = 14;\n  SCREENSHOT = 15;\n  LOCKSCREEN = 16;\n  RECOMMEND = 17;\n  PLAYBACKSPEED = 18;\n  DEFINITION = 19;\n  SELECTIONS = 20;\n  NEXT = 21;\n  EDITDM = 22;\n  SMALLWINDOW = 23;\n  SHAKE = 24;\n  OUTERDM = 25;\n  INNERDM = 26;\n  PANORAMA = 27;\n  DOLBY = 28;\n  COLORFILTER = 29;\n  LOSSLESS = 30;\n  FREYAENTER = 31;\n  FREYAFULLENTER = 32;\n  SKIPOPED = 33;\n  RECORDSCREEN = 34;\n  DUBBING = 35;\n  LISTEN = 36;\n}\n\n//\nmessage ConfValue {\n  oneof value {\n    //\n    int32 switch_val = 1;\n    //\n    int32 selected_val = 2;\n  }\n}\n\n// Dash条目\nmessage DashItem {\n  // 清晰度\n  uint32 id = 1;\n  // 主线流\n  string base_url = 2;\n  // 备用流\n  repeated string backup_url = 3;\n  // 带宽\n  uint32 bandwidth = 4;\n  // 编码id\n  uint32 codecid = 5;\n  // md5\n  string md5 = 6;\n  // 大小\n  uint64 size = 7;\n  // 帧率\n  string frame_rate = 8;\n  // DRM密钥\n  string widevine_pssh = 9;\n}\n\n// 视频流信息: dash流\nmessage DashVideo {\n  // 主线流\n  string base_url = 1;\n  // 备用流\n  repeated string backup_url = 2;\n  // 带宽\n  uint32 bandwidth = 3;\n  // 编码id\n  uint32 codecid = 4;\n  // md5\n  string md5 = 5;\n  // 大小\n  uint64 size = 6;\n  // 伴音质量id\n  uint32 audio_id = 7;\n  // 是否非全二压\n  bool no_rexcode = 8;\n  // 帧率\n  string frame_rate = 9;\n  // 宽\n  int32 width = 10;\n  // 高\n  int32 height = 11;\n  // DRM密钥\n  string widevine_pssh = 12;\n}\n\n//\nmessage DeviceConf {\n  ConfValue conf_value = 1;\n}\n\n//\nmessage Dialog {\n  //\n  int32 style_type = 1;\n  //\n  BackgroundInfo background_info = 2;\n  //\n  TextInfo title = 3;\n  //\n  TextInfo subtitle = 4;\n  //\n  ImageInfo image = 5;\n  //\n  repeated ButtonInfo button = 6;\n  //\n  ButtonInfo bottom_desc = 7;\n  //\n  Report report = 8;\n  //\n  int32 count_down_sec = 9;\n  //\n  TextInfo right_bottom_desc = 10;\n  //\n  repeated BottomDisplay bottom_display = 11;\n  //\n  ExtData ext_data = 12;\n  //\n  int32 limit_action_type = 13;\n}\n\n// 当前分辨率信息\nmessage Dimension {\n  // 宽\n  int32 width = 1;\n  // 长\n  int32 height = 2;\n  // 旋转角度\n  int32 rotate = 3;\n}\n\n// 杜比伴音流信息\nmessage DolbyItem {\n  // 杜比类型\n  enum Type {\n    NONE = 0;   // NONE\n    COMMON = 1; // 普通杜比音效\n    ATMOS = 2;  // 全景杜比音效\n  }\n  // 杜比类型\n  Type type = 1;\n  // 音频流\n  repeated DashItem audio = 2;\n}\n\n// DRM类型\nenum DrmTechType {\n  //\n  UNKNOWN_DRM = 0;\n  //\n  FAIR_PLAY = 1;\n  //\n  WIDE_VINE = 2;\n  // 哔哩哔哩自研DRM\n  BILI_DRM = 3;\n}\n\nenum Effects {\n  //\n  EFFECTS_UNKNOWN = 0;\n  //\n  GAUSSIAN_BLUR = 1;\n  //\n  HALF_ALPHA = 2;\n}\n\n// 事件\nmessage Event {\n  // 震动\n  Shake shake = 1;\n}\n\n//\nmessage ExtData {\n  //\n  ExtDataType type = 1;\n  //\n  oneof data {\n    PlayListInfo play_list_info = 2;\n  }\n}\n\nenum ExtDataType {\n  //\n  EXT_DATA_TYPE_UNKNOWN = 0;\n  //\n  PLAY_LIST = 1;\n}\n\n// ? 错误码补充信息\nmessage ExtraContent {\n  //\n  string disable_reason = 1;\n  //\n  int64 disable_code = 2;\n}\n\n//\nmessage GradientColor {\n  //\n  string start_color = 1;\n  //\n  string end_color = 2;\n}\n\n//\nenum GuideStyle {\n  //\n  STYLE_UNKNOWN = 0;\n  //\n  HORIZONTAL_IMAGE = 1;\n  //\n  VERTICAL_TEXT = 2;\n  //\n  SIMPLE_TEXT = 3;\n  //\n  CHARGING_TEXT = 4;\n}\n\n// 播放历史\nmessage History {\n  //\n  HistoryInfo current_video = 1;\n  //\n  HistoryInfo related_video = 2;\n}\n\n//\nmessage HistoryInfo {\n  //\n  int64 progress = 1;\n  //\n  int64 last_play_cid = 2;\n  //\n  Toast toast = 3;\n  //\n  Toast toast_without_time = 4;\n  //\n  int64 last_play_aid = 5;\n}\n\n//\nmessage ImageInfo {\n  //\n  string url = 1;\n}\n\n//\nmessage Interaction {\n  //\n  Node history_node = 1;\n  //\n  int64 graph_version = 2;\n  //\n  string msg = 3;\n  //\n  int64 mark = 4;\n}\n\nenum LimitActionType {\n  //\n  LAT_UNKNOWN = 0;\n  //\n  SHOW_LIMIT_DIALOG = 1;\n  //\n  SKIP_CURRENT_EP = 2;\n}\n\n// HIRES伴音流信息\nmessage LossLessItem {\n  // 是否为hires\n  bool is_lossless_audio = 1;\n  // 音频流信息\n  DashItem audio = 2;\n  // 是否需要大会员\n  bool need_vip = 3;\n}\n\n//\nmessage Node {\n  //\n  int64 node_id = 1;\n  //\n  string title = 2;\n  //\n  int64 cid = 3;\n}\n\n//\nmessage PlayArc {\n  //\n  BizType video_type = 1;\n  //\n  uint64 aid = 2;\n  //\n  uint64 cid = 3;\n  //\n  DrmTechType drm_tech_type = 4;\n  //\n  ArcType arc_type = 5;\n  //\n  Interaction interaction = 6;\n  //\n  Dimension dimension = 7;\n  //\n  int64 duration = 8;\n  //\n  bool is_preview = 9;\n}\n\n// 播放页信息-响应: PlayArcConf\nmessage PlayArcConf {\n  map<int32, ArcConf> arc_confs = 1;\n}\n\n//\nmessage PlayDeviceConf {\n  //\n  map<int32, DeviceConf> device_confs = 1;\n}\n\n// 错误码\nenum PlayErr {\n  NoErr = 0;                   //\n  WithMultiDeviceLoginErr = 1; // 管控类型的错误码\n}\n\n//\nmessage PlayList {\n  //\n  int64 season_id = 1;\n  //\n  string title = 2;\n  //\n  string cover = 3;\n  //\n  string link = 4;\n  //\n  BadgeInfo badge_info = 5;\n}\n\n//\nmessage PlayListInfo {\n  //\n  repeated PlayList play_list = 2;\n}\n\n// 视频下方广告 Banner\nmessage PromptBar {\n  //\n  TextInfo title = 1;\n  //\n  TextInfo subtitle = 2;\n  //\n  string sub_title_icon = 3;\n  //\n  string bg_image = 4;\n  //\n  GradientColor bg_gradient_color = 5;\n  //\n  repeated ButtonInfo button = 6;\n  //\n  Report report = 7;\n  //\n  string full_screen_ip_icon = 8;\n  //\n  GradientColor full_screen_bg_gradient_color = 9;\n}\n\n// 播放页信息-响应: 高画质试看信息\nmessage QnTrialInfo {\n  // 能否试看高画质\n  bool trial_able = 1;\n  //\n  int32 remaining_times = 2;\n  //\n  int32 start = 3;\n  //\n  int32 time_length = 4;\n  //\n  Toast start_toast = 5;\n  //\n  Toast end_toast = 6;\n  //\n  Button quality_open_tip_btn = 8;\n}\n\n//\nmessage Report {\n  //\n  string show_event_id = 1;\n  //\n  string click_event_id = 2;\n  //\n  string extends = 3;\n}\n\n// Dash Response, 未使用\nmessage ResponseDash {\n  repeated DashItem video = 1;\n  repeated DashItem audio = 2;\n}\n\n// 分段流条目\nmessage ResponseUrl {\n  // 分段序号\n  uint32 order = 1;\n  // 分段时长\n  uint64 length = 2;\n  // 分段大小\n  uint64 size = 3;\n  // 主线流\n  string url = 4;\n  // 备用流\n  repeated string backup_url = 5;\n  // md5\n  string md5 = 6;\n}\n\n// 方案\nmessage Scheme {\n  enum ActionType {\n    UNKNOWN = 0;\n    SHOW_TOAST = 1;\n  }\n  //\n  ActionType action_type = 1;\n  //\n  string toast = 2;\n}\n\n// 视频流信息: 分段流\nmessage SegmentVideo {\n  repeated ResponseUrl segment = 1;\n}\n\n// 震动\nmessage Shake {\n  //\n  string file = 1;\n}\n\nenum ShowStyleType {\n  //\n  SHOW_STYLE_TYPE_UNKNOWN = 0;\n  //\n  SHOW_STYLE_TYPE_ORDINARY = 1;\n  //\n  SHOW_STYLE_TYPE_RESIDENT = 2;\n}\n\n// 视频流信息\nmessage Stream {\n  // 元数据\n  StreamInfo stream_info = 1;\n  // 流数据\n  oneof content {\n    // dash流\n    DashVideo dash_video = 2;\n    // 分段流\n    SegmentVideo segment_video = 3;\n  }\n}\n\n// 视频流信息: 元数据\nmessage StreamInfo {\n  // 清晰度\n  uint32 quality = 1;\n  // 格式\n  string format = 2;\n  // 格式描述\n  string description = 3;\n  // 错误码\n  uint32 err_code = 4;\n  // 不满足条件信息\n  StreamLimit limit = 5;\n  // 是否需要vip\n  bool need_vip = 6;\n  // 是否需要登录\n  bool need_login = 7;\n  // 是否完整\n  bool intact = 8;\n  // 是否非全二压\n  bool no_rexcode = 9;\n  // 清晰度属性位\n  int64 attribute = 10;\n  // 新版格式描述\n  string new_description = 11;\n  // 格式文字\n  string display_desc = 12;\n  // 新版格式描述备注\n  string superscript = 13;\n  //\n  bool vip_free = 14;\n  //\n  string subtitle = 15;\n  // 方案\n  Scheme scheme = 16;\n  // 支持drm\n  bool support_drm = 17;\n}\n\n// 视频流信息: 流媒体元数据: 清晰度不满足条件信息\nmessage StreamLimit {\n  // 标题\n  string title = 1;\n  // 跳转地址\n  string uri = 2;\n  // 提示信息\n  string msg = 3;\n}\n\n//\nmessage TaskParam {\n  //\n  string task_type = 1;\n  //\n  int64 activity_id = 2;\n  //\n  int64 tips_id = 3;\n}\n\n//\nmessage TextInfo {\n  //\n  string text = 1;\n  //\n  string text_color = 2;\n  //\n  string text_color_night = 3;\n}\n\n// Toast信息\nmessage Toast {\n  // toast文案\n  string text = 1;\n  // toast按钮\n  Button button = 2;\n}\n\nenum ToastType {\n  //\n  TOAST_TYPE_UNKNOWN = 0;\n  //\n  VIP_CONTENT_REMIND = 1;\n  //\n  VIP_DEFINITION_REMIND = 2;\n  //\n  VIP_DEFINITION_GUIDE = 3;\n  //\n  OGV_VIDEO_START_TOAST = 4;\n  //\n  CHARGING_TOAST = 5;\n}\n\n//\nenum UnsupportScene {\n  //\n  UNKNOWN_SCENE = 0;\n  //\n  PREMIERE = 1;\n}\n\n// 播放页信息-请求: 音视频VOD\nmessage VideoVod {\n  // 视频aid\n  int64 aid = 1;\n  // 视频cid\n  int64 cid = 2;\n  // 清晰度\n  uint64 qn = 3;\n  // 视频流版本\n  int32 fnver = 4;\n  // 视频流格式\n  int32 fnval = 5;\n  // 下载模式\n  // 0:播放 1:flv下载 2:dash下载\n  uint32 download = 6;\n  // 流url强制是用域名\n  // 0:允许使用ip 1:使用http 2:使用https\n  int32 force_host = 7;\n  // 是否4K\n  bool fourk = 8;\n  // 视频编码\n  CodeType prefer_codec_type = 9;\n  // 响度均衡\n  uint64 voice_balance = 10;\n}\n\nmessage ViewInfo {\n  //\n  map<string, Dialog> dialog_map = 1;\n  //\n  PromptBar prompt_bar = 2;\n  //\n  repeated ComprehensiveToast toasts = 3;\n}\n\n// 播放页信息-响应: VOD音视频信息\nmessage VodInfo {\n  // 视频清晰度\n  uint32 quality = 1;\n  // 视频格式\n  string format = 2;\n  // 视频时长\n  uint64 timelength = 3;\n  // 视频编码id\n  uint32 video_codecid = 4;\n  // 视频流\n  repeated Stream stream_list = 5;\n  // 伴音流\n  repeated DashItem dash_audio = 6;\n  // 杜比伴音流\n  DolbyItem dolby = 7;\n  // 响度均衡操作信息\n  VolumeInfo volume = 8;\n  // HIRES伴音流信息\n  LossLessItem loss_less_item = 9;\n  // 是否支持投屏\n  bool support_project = 10;\n}\n\n// 响度均衡操作信息\nmessage VolumeInfo {\n  // Measured integrated loudness 实际综合响度\n  double measured_i = 1;\n  // Measured loudness range 实际响度范围\n  double measured_lra = 2;\n  // Measured true peak 实际响度真峰值\n  double measured_tp = 3;\n  // Measured threshold 实际响度阈值\n  double measured_threshold = 4;\n  // Target offset gain(Gain is applied before the true-peak limiter) 目标增益Offset(增益在真实峰值限制器之前应用)\n  double target_offset = 5;\n  // Target integrated loudness 目标综合响度\n  double target_i = 6;\n  // Target true peak 目标响度真峰值\n  double target_tp = 7;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/polymer/app/search/v1/search.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.polymer.app.search.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/app/archive/middleware/v1/preload.proto\";\nimport \"bilibili/pagination/pagination.proto\";\n\n//\nservice Search {\n  // 搜索所有类型结果\n  rpc SearchAll(SearchAllRequest) returns (SearchAllResponse);\n  // 搜索指定类型结果\n  rpc SearchByType(SearchByTypeRequest) returns (SearchByTypeResponse);\n  //\n  rpc SearchComic(SearchComicRequest) returns (SearchComicResponse);\n}\n\n//\nmessage Args {\n  //\n  int32 online = 1;\n  //\n  string rname = 2;\n  //\n  int64 room_id = 3;\n  //\n  string tname = 4;\n  //\n  int64 up_id = 5;\n  //\n  string up_name = 6;\n  //\n  int64 rid = 7;\n  //\n  int64 tid = 8;\n  //\n  int64 aid = 9;\n}\n\n//\nmessage Avatar {\n  //\n  string cover = 1;\n  //\n  string event = 2;\n  //\n  string event_v2 = 3;\n  //\n  string text = 4;\n  //\n  int64 up_id = 5;\n  //\n  string uri = 6;\n  //\n  int32 face_nft_new = 7;\n  //\n  NftFaceIcon nft_face_icon = 8;\n}\n\n//\nmessage AvItem {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string uri = 3;\n  //\n  string ctime_label = 4;\n  //\n  string duration = 5;\n  //\n  int64 play = 6;\n  //\n  int64 danmaku = 7;\n  //\n  int32 ctime = 8;\n  //\n  string goto = 9;\n  //\n  string param = 10;\n  //\n  int32 position = 11;\n  //\n  string ctime_label_v2 = 12;\n}\n\n//\nmessage Background {\n  //\n  int32 show = 1;\n  //\n  string bg_pic_url = 2;\n  //\n  string fg_pic_url = 3;\n}\n\n//\nmessage Badge {\n  //\n  string text = 1;\n  //\n  string bg_cover = 2;\n}\n\n//\nmessage Badge2 {\n  //\n  string bg_cover = 1;\n  //\n  string text = 2;\n}\n\n//\nmessage BottomButton {\n  //\n  string desc = 1;\n  //\n  string link = 2;\n}\n\n//\nmessage BrandADAccount {\n  //\n  string param = 1;\n  //\n  string goto = 2;\n  //\n  int64 mid = 3;\n  //\n  string name = 4;\n  //\n  string face = 5;\n  //\n  string sign = 6;\n  //\n  Relation relation = 7;\n  //\n  int64 roomid = 8;\n  //\n  int64 live_status = 9;\n  //\n  string live_link = 10;\n  //\n  OfficialVerify official_verify = 11;\n  //\n  VipInfo vip = 12;\n  //\n  string uri = 13;\n  //\n  int32 face_nft_new = 14;\n}\n\n//\nmessage BrandADArc {\n  //\n  string param = 1;\n  //\n  string goto = 2;\n  //\n  int64 aid = 3;\n  //\n  int64 play = 4;\n  //\n  int64 reply = 5;\n  //\n  string duration = 6;\n  //\n  string author = 7;\n  //\n  string title = 8;\n  //\n  string uri = 9;\n  //\n  string cover = 10;\n}\n\n//\nmessage Button {\n  //\n  string text = 1;\n  //\n  string param = 2;\n  //\n  string uri = 3;\n  //\n  string event = 4;\n  //\n  int32 selected = 5;\n  //\n  int32 type = 6;\n  //\n  string event_v2 = 7;\n  //\n  Relation relation = 8;\n}\n\n//\nmessage ButtonMeta {\n  //\n  string icon = 1;\n  //\n  string text = 2;\n  //\n  string button_status = 3;\n  //\n  string toast = 4;\n}\n\n//\nmessage CardBusinessBadge {\n  //\n  GotoIcon goto_icon = 1;\n  //\n  ReasonStyle badge_style = 2;\n}\n\n//\nenum CategorySort {\n  CATEGORY_SORT_DEFAULT = 0; //\n  CATEGORY_SORT_PUBLISH_TIME = 1; //\n  CATEGORY_SORT_CLICK_COUNT = 2; //\n  CATEGORY_SORT_COMMENT_COUNT = 3; //\n  CATEGORY_SORT_LIKE_COUNT = 4; //\n}\n\n//\nmessage ChannelLabel {\n  //\n  string text = 1;\n  //\n  string uri = 2;\n}\n\n//\nmessage ChannelMixedItem {\n  //\n  int64 id = 1;\n  //\n  int32 cover_left_icon1 = 2;\n  //\n  string cover_left_text1 = 3;\n  //\n  string cover = 4;\n  //\n  string goto = 5;\n  //\n  string param = 6;\n  //\n  string uri = 7;\n  //\n  string title = 8;\n  //\n  Badge2 badge = 9;\n}\n\n//\nmessage CheckMore {\n  //\n  string content = 1;\n  //\n  string uri = 2;\n}\n\n//\nmessage CloudGameParams {\n  //\n  int64 source_from = 1;\n  //\n  string scene = 2;\n}\n\n//\nmessage DetailsRelationItem {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string cover_left_text = 3;\n  //\n  ReasonStyle cover_badge_style = 4;\n  //\n  string module_pos = 5;\n  //\n  string goto = 6;\n  //\n  string param = 7;\n  //\n  string uri = 8;\n  //\n  int32 position = 9;\n  //\n  string cover_left_text_v2 = 10;\n  //\n  ReasonStyle cover_badge_style_v2 = 11;\n}\n\n//\nmessage DislikeReason {\n  //\n  int32 id = 1;\n  //\n  string name = 2;\n}\n\n//\nmessage DisplayOption {\n  //\n  int32 video_title_row = 1;\n  //\n  int32 search_page_visual_opti = 2;\n}\n\n//\nmessage DyTopic {\n  //\n  string title = 1;\n  //\n  string uri = 2;\n}\n\n//\nmessage EasterEgg {\n  //\n  int32 id = 1;\n  //\n  int32 show_count = 2;\n  //\n  int32 type = 3;\n  //\n  string url = 4;\n  //\n  int32 close_count = 5;\n  //\n  int32 mask_transparency = 6;\n  //\n  string mask_color = 7;\n  //\n  int32 pic_type = 8;\n  //\n  int32 show_time = 9;\n  //\n  string source_url = 10;\n  //\n  string source_md5 = 11;\n  //\n  int32 source_size = 12;\n}\n\n//\nmessage Episode {\n  //\n  string uri = 1;\n  //\n  string param = 2;\n  //\n  string index = 3;\n  //\n  repeated ReasonStyle badges = 4;\n  //\n  int32 position = 5;\n}\n\n//\nmessage EpisodeNew {\n  //\n  string title = 1;\n  //\n  string uri = 2;\n  //\n  string param = 3;\n  //\n  int32 is_new = 4;\n  //\n  repeated ReasonStyle badges = 5;\n  //\n  int32 type = 6;\n  //\n  int32 position = 7;\n  //\n  string cover = 8;\n  //\n  string label = 9;\n}\n\n//\nmessage ExtraLink {\n  //\n  string text = 1;\n  //\n  string uri = 2;\n}\n\n//\nmessage FollowButton {\n  //\n  string icon = 1;\n  //\n  map<string, string> texts = 2;\n  //\n  string status_report = 3;\n}\n\n//\nmessage FullTextResult {\n  //\n  int32 type = 1;\n  //\n  string show_text = 2;\n  //\n  int64 jump_start_progress = 3;\n  //\n  string jump_uri = 4;\n}\n\n//\nmessage GotoIcon {\n  //\n  string icon_url = 1;\n  //\n  string icon_night_url = 2;\n  //\n  int32 icon_width = 3;\n  //\n  int32 icon_height = 4;\n}\n\n//\nmessage InlineProgressBar {\n  //\n  string icon_drag = 1;\n  //\n  string icon_drag_hash = 2;\n  //\n  string icon_stop = 3;\n  //\n  string icon_stop_hash = 4;\n}\n\n//\nmessage InlineThreePointPanel {\n  //\n  int32 panel_type = 1;\n  //\n  string share_id = 2;\n  //\n  string share_origin = 3;\n  //\n  repeated ShareButtonItem functional_buttons = 4;\n}\n\nmessage Item {\n  //\n  string uri = 1;\n  //\n  string param = 2;\n  //\n  string goto = 3;\n  //\n  string linktype = 4;\n  //\n  int32 position = 5;\n  //\n  string trackid = 6;\n  //\n  oneof card_item {\n    //\n    SearchSpecialCard special = 7;\n    //\n    SearchArticleCard article = 8;\n    //\n    SearchBannerCard banner = 9;\n    //\n    SearchLiveCard live = 10;\n    //\n    SearchGameCard game = 11;\n    //\n    SearchPurchaseCard purchase = 12;\n    //\n    SearchRecommendWordCard recommend_word = 13;\n    //\n    SearchDynamicCard dynamic = 14;\n    //\n    SearchNoResultSuggestWordCard suggest_keyword = 15;\n    //\n    SearchSpecialGuideCard special_guide = 16;\n    //\n    SearchComicCard comic = 17;\n    //\n    SearchNewChannelCard channel_new = 18;\n    //\n    SearchOgvCard ogv_card = 19;\n    //\n    SearchOgvRelationCard bangumi_relates = 20;\n    //\n    SearchOgvRecommendCard find_more = 21;\n    //\n    SearchSportCard esport = 22;\n    //\n    SearchAuthorNewCard author_new = 23;\n    //\n    SearchTipsCard tips = 24;\n    //\n    SearchAdCard cm = 25;\n    //\n    SearchPediaCard pedia_card = 26;\n    //\n    SearchUgcInlineCard ugc_inline = 27;\n    //\n    SearchLiveInlineCard live_inline = 28;\n    //\n    SearchTopGameCard top_game = 29;\n    //\n    SearchOlympicGameCard sports = 30;\n    //\n    SearchOlympicWikiCard pedia_card_inline = 31;\n    //\n    SearchRecommendTipCard recommend_tips = 32;\n    //\n    SearchCollectionCard collection_card = 33;\n    //\n    SearchOgvChannelCard ogv_channel = 34;\n    //\n    SearchOgvInlineCard ogv_inline = 35;\n    //\n    SearchUpperCard author = 36;\n    //\n    SearchVideoCard av = 37;\n    //\n    SearchBangumiCard bangumi = 38;\n    //\n    SearchSportInlineCard esports_inline = 39;\n  }\n}\n\n//\nmessage LikeResource {\n  //\n  string url = 1;\n  //\n  string content_hash = 2;\n}\n\n//\nmessage LiveBadgeResource {\n  //\n  string text = 1;\n  //\n  string animation_url = 2;\n  //\n  string animation_url_hash = 3;\n  //\n  string background_color_light = 4;\n  //\n  string background_color_night = 5;\n  //\n  int64 alpha_light = 6;\n  //\n  int64 alpha_night = 7;\n  //\n  string font_color = 8;\n}\n\n//\nmessage Mask {\n  //\n  Avatar avatar = 1;\n  //\n  Button button = 2;\n}\n\n//\nmessage MatchInfoObj {\n  //\n  int64 id = 1;\n  //\n  int32 status = 2;\n  //\n  string match_stage = 3;\n  //\n  MatchTeam team1 = 4;\n  //\n  MatchTeam team2 = 5;\n  //\n  MatchItem match_label = 6;\n  //\n  MatchItem match_time = 7;\n  //\n  MatchItem match_button = 8;\n}\n\n//\nmessage MatchItem {\n  //\n  int32 state = 1;\n  //\n  string text = 2;\n  //\n  string text_color = 3;\n  //\n  string text_color_night = 4;\n  //\n  string uri = 5;\n  //\n  string live_link = 6;\n  //\n  Texts texts = 7;\n}\n\n//\nmessage MatchTeam {\n  //\n  int64 id = 1;\n  //\n  string title = 2;\n  //\n  string cover = 3;\n  //\n  int32 score = 4;\n}\n\n//\nmessage Nav {\n  //\n  string name = 1;\n  //\n  int32 total = 2;\n  //\n  int32 pages = 3;\n  //\n  int32 type = 4;\n}\n\n//\nmessage Navigation {\n  //\n  int64 id = 1;\n  //\n  repeated Navigation children = 2;\n  //\n  repeated Navigation inline_children = 3;\n  //\n  string title = 4;\n  //\n  string uri = 5;\n  //\n  NavigationButton button = 6;\n}\n\n//\nmessage NavigationButton {\n  //\n  int64 type = 1;\n  //\n  string text = 2;\n  //\n  string uri = 3;\n}\n\n//\nmessage NftFaceIcon {\n  //\n  int32 region_type = 1;\n  //\n  string icon = 2;\n  //\n  int32 show_status = 3;\n}\n\n//\nmessage Notice {\n  //\n  int64 mid = 1;\n  //\n  int64 notice_id = 2;\n  //\n  string content = 3;\n  //\n  string url = 4;\n  //\n  int64 notice_type = 5;\n  //\n  string icon = 6;\n  //\n  string icon_night = 7;\n  //\n  string text_color = 8;\n  //\n  string text_color_night = 9;\n  //\n  string bg_color = 10;\n  //\n  string bg_color_night = 11;\n}\n\n//\nmessage OfficialVerify {\n  //\n  int32 type = 1;\n  //\n  string desc = 2;\n}\n\n//\nmessage OgvCardUI {\n  //\n  string background_image = 1;\n  //\n  string gaussian_blur_value = 2;\n  //\n  string module_color = 3;\n}\n\n//\nmessage OgvClipInfo {\n  //\n  int64 play_start_time = 1;\n  //\n  int64 play_end_time = 2;\n}\n\n//\nmessage OgvRecommendWord {\n  //\n  string title = 1;\n  //\n  string goto = 2;\n  //\n  string param = 3;\n  //\n  string uri = 4;\n}\n\n//\nmessage PediaCover {\n  //\n  int64 cover_type = 1;\n  //\n  string cover_sun_url = 2;\n  //\n  string cover_night_url = 3;\n  //\n  int32 cover_width = 4;\n  //\n  int32 cover_height = 5;\n}\n\n//\nmessage PlayerArgs {\n  //\n  int32 is_live = 1;\n  //\n  int64 aid = 2;\n  //\n  int64 cid = 3;\n  //\n  int32 sub_type = 4;\n  //\n  int64 room_id = 5;\n  //\n  int64 ep_id = 7;\n  //\n  int32 is_preview = 8;\n  //\n  string type = 9;\n  //\n  int32 duration = 10;\n  //\n  int64 season_id = 11;\n  //\n  int32 report_required_play_duration = 12;\n  //\n  int32 report_required_time = 13;\n  //\n  int32 manual_play = 14;\n  //\n  bool hide_play_button = 15;\n  //\n  int32 content_mode = 16;\n  //\n  int32 report_history = 17;\n}\n\n//\nmessage PlayerWidget {\n  //\n  string title = 1;\n  //\n  string desc = 2;\n}\n\n//\nmessage RankInfo {\n  //\n  string search_night_icon_url = 1;\n  //\n  string search_day_icon_url = 2;\n  //\n  string search_bkg_night_color = 3;\n  //\n  string search_bkg_day_color = 4;\n  //\n  string search_font_night_color = 5;\n  //\n  string search_font_day_color = 6;\n  //\n  string rank_content = 7;\n  //\n  string rank_link = 8;\n}\n\n//\nmessage RcmdReason {\n  //\n  string content = 1;\n}\n\n//\nmessage ReasonStyle {\n  //\n  string text = 1;\n  //\n  string text_color = 2;\n  //\n  string text_color_night = 3;\n  //\n  string bg_color = 4;\n  //\n  string bg_color_night = 5;\n  //\n  string border_color = 6;\n  //\n  string border_color_night = 7;\n  //\n  int32 bg_style = 8;\n}\n\n//\nmessage RecommendWord {\n  //\n  string param = 1;\n  //\n  string type = 2;\n  //\n  string title = 3;\n  //\n  string from_source = 4;\n}\n\n//\nmessage Relation {\n  //\n  int32 status = 1;\n}\n\n//\nmessage RightTopLiveBadge {\n  //\n  int32 live_status = 1;\n  //\n  LiveBadgeResource in_live = 2;\n  //\n  string live_stats_desc = 3;\n}\n\n//\nmessage SearchAdCard {\n  //\n  string json_str = 1;\n}\n\n//\nmessage SearchAllRequest {\n  //\n  string keyword = 1;\n  //\n  int32 order = 2;\n  //\n  string tid_list = 3;\n  //\n  string duration_list = 4;\n  //\n  string extra_word = 5;\n  //\n  string from_source = 6;\n  //\n  int32 is_org_query = 7;\n  //\n  int32 local_time = 8;\n  //\n  string ad_extra = 9;\n  //\n  bilibili.pagination.Pagination pagination = 10;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 11;\n}\n\n//\nmessage SearchAllResponse {\n  //\n  string keyword = 1;\n  //\n  string trackid = 2;\n  //\n  repeated Nav nav = 3;\n  //\n  repeated Item item = 4;\n  //\n  EasterEgg easter_egg = 5;\n  //\n  string exp_str = 6;\n  //\n  repeated string extra_word_list = 7;\n  //\n  string org_extra_word = 8;\n  //\n  int64 select_bar_type = 9;\n  //\n  int64 new_search_exp_num = 10;\n  //\n  bilibili.pagination.PaginationReply pagination = 11;\n  //\n  DisplayOption app_display_option = 12;\n  //\n  map<string, string> annotation = 13;\n}\n\n//\nmessage SearchArticleCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  int64 play = 3;\n  //\n  int32 like = 4;\n  //\n  int32 reply = 5;\n  //\n  repeated string image_urls = 6;\n  //\n  string author = 7;\n  //\n  int32 template_id = 8;\n  //\n  int64 id = 9;\n  //\n  int64 mid = 10;\n  //\n  string name = 11;\n  //\n  string desc = 12;\n  //\n  int64 view = 13;\n}\n\n//\nmessage SearchAuthorNewCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  int32 live_face = 3;\n  //\n  string live_uri = 4;\n  //\n  string live_link = 5;\n  //\n  int32 fans = 6;\n  //\n  int32 level = 7;\n  //\n  string sign = 8;\n  //\n  bool is_up = 9;\n  //\n  int32 archives = 10;\n  //\n  int64 mid = 11;\n  //\n  int64 roomid = 12;\n  //\n  Relation relation = 13;\n  //\n  OfficialVerify official_verify = 14;\n  //\n  int32 face_nft_new = 15;\n  //\n  NftFaceIcon nft_face_icon = 16;\n  //\n  int32 is_senior_member = 17;\n  //\n  Background background = 18;\n  //\n  int32 av_style = 19;\n  //\n  Space space = 20;\n  //\n  repeated AvItem av_items = 21;\n  //\n  Notice notice = 22;\n  //\n  SharePlane share_plane = 23;\n  //\n  string inline_type = 24;\n  //\n  SearchInlineData inline_live = 25;\n  //\n  int32 is_inline_live = 26;\n  //\n  repeated ThreePoint three_point = 27;\n  //\n  int32 live_status = 28;\n  //\n  VipInfo vip = 29;\n}\n\n//\nmessage SearchBangumiCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  int32 media_type = 3;\n  //\n  int32 play_state = 4;\n  //\n  string area = 5;\n  //\n  string style = 6;\n  //\n  string styles = 7;\n  //\n  string cv = 8;\n  //\n  double rating = 9;\n  //\n  int32 vote = 10;\n  //\n  string target = 11;\n  //\n  string staff = 12;\n  //\n  string prompt = 13;\n  //\n  int64 ptime = 14;\n  //\n  string season_type_name = 15;\n  //\n  repeated Episode episodes = 16;\n  //\n  int32 is_selection = 17;\n  //\n  int32 is_atten = 18;\n  //\n  string label = 19;\n  //\n  int64 season_id = 20;\n  //\n  string out_name = 21;\n  //\n  string out_icon = 22;\n  //\n  string out_url = 23;\n  //\n  repeated ReasonStyle badges = 24;\n  //\n  int32 is_out = 25;\n  //\n  repeated EpisodeNew episodes_new = 26;\n  //\n  WatchButton watch_button = 27;\n  //\n  string selection_style = 28;\n  //\n  CheckMore check_more = 29;\n  //\n  FollowButton follow_button = 30;\n  //\n  ReasonStyle style_label = 31;\n  //\n  repeated ReasonStyle badges_v2 = 32;\n  //\n  string styles_v2 = 33;\n}\n\n//\nmessage SearchBannerCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n}\n\n//\nmessage SearchByTypeRequest {\n  enum CategorySort {\n    CATEGORY_SORT_DEFAULT = 0;\n    CATEGORY_SORT_PUBLISH_TIME = 1;\n    CATEGORY_SORT_CLICK_COUNT = 2;\n    CATEGORY_SORT_COMMENT_COUNT = 3;\n    CATEGORY_SORT_LIKE_COUNT = 4;\n  }\n  enum UserType {\n    ALL = 0;\n    UP = 1;\n    NORMAL_USER = 2;\n    AUTHENTICATED_USER = 3;\n  }\n  enum UserSort {\n    USER_SORT_DEFAULT = 0;\n    USER_SORT_FANS_DESCEND = 1;\n    USER_SORT_FANS_ASCEND = 2;\n    USER_SORT_LEVEL_DESCEND = 3;\n    USER_SORT_LEVEL_ASCEND = 4;\n  }\n  // 搜索目标类型, 番剧为7\n  int32 type = 1;\n  // 关键词\n  string keyword = 2;\n  //\n  CategorySort category_sort = 3;\n  //\n  int64 category_id = 4;\n  //\n  UserType user_type = 5;\n  //\n  UserSort user_sort = 6;\n  //\n  bilibili.pagination.Pagination pagination = 7;\n  //\n  bilibili.app.archive.middleware.v1.PlayerArgs player_args = 8;\n}\n\n//\nmessage SearchByTypeResponse {\n  // 追踪id\n  string trackid = 1;\n  // 当前页码\n  int32 pages = 2;\n  //\n  string exp_str = 3;\n  // 搜索关键词\n  string keyword = 4;\n  // 是否为推荐结果\n  int32 result_is_recommend = 5;\n  // 搜索结果条目\n  repeated Item items = 6;\n  // 分页信息\n  bilibili.pagination.PaginationReply pagination = 7;\n  //\n  map<string, string> annotation = 8;\n}\n\n//\nmessage SearchCollectionCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string author = 3;\n  //\n  repeated AvItem av_items = 4;\n  //\n  BottomButton bottom_button = 5;\n  //\n  string collection_icon = 6;\n  //\n  string show_card_desc1 = 7;\n  //\n  string show_card_desc2 = 8;\n}\n\n//\nmessage SearchComicCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string name = 3;\n  //\n  string style = 4;\n  //\n  string comic_url = 5;\n  //\n  string badge = 6;\n}\n\n//\nmessage SearchComicInfo {\n  //\n  string uri = 1;\n  //\n  string param = 2;\n  //\n  SearchComicCard comic = 3;\n}\n\n//\nmessage SearchComicRequest {\n  //\n  string id_list = 1;\n}\n\n//\nmessage SearchComicResponse {\n  //\n  repeated SearchComicInfo items = 1;\n}\n\n//\nmessage SearchDynamicCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  int32 cover_count = 3;\n  //\n  repeated string covers = 4;\n  //\n  Upper upper = 5;\n  //\n  Stat stat = 6;\n  //\n  repeated DyTopic dy_topic = 7;\n}\n\n//\nmessage SearchGameCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string reserve = 3;\n  //\n  float rating = 4;\n  //\n  string tags = 5;\n  //\n  string notice_name = 6;\n  //\n  string notice_content = 7;\n  //\n  string gift_content = 8;\n  //\n  string gift_url = 9;\n  //\n  int32 reserve_status = 10;\n  //\n  RankInfo rank_info = 11;\n  //\n  string special_bg_color = 12;\n  //\n  CloudGameParams cloud_game_params = 13;\n  //\n  bool show_cloud_game_entry = 14;\n}\n\n//\nmessage SearchInlineData {\n  //\n  string uri = 1;\n  //\n  string title = 2;\n  //\n  PlayerArgs player_args = 3;\n  //\n  int32 can_play = 4;\n  //\n  Args args = 5;\n  //\n  string card_goto = 6;\n  //\n  string card_type = 7;\n  //\n  string cover = 8;\n  //\n  int32 cover_left_icon1 = 9;\n  //\n  int32 cover_left_icon2 = 10;\n  //\n  string cover_left_text1 = 11;\n  //\n  string cover_left_text2 = 12;\n  //\n  UpArgs up_args = 13;\n  //\n  string extra_uri = 14;\n  //\n  bool is_fav = 15;\n  //\n  bool is_coin = 16;\n  //\n  string goto = 17;\n  //\n  Share share = 18;\n  //\n  ThreePoint2 three_point = 19;\n  //\n  repeated ThreePointV2 three_point_v2 = 20;\n  //\n  SharePlane share_plane = 21;\n  //\n  InlineThreePointPanel three_point_meta = 22;\n  //\n  Avatar avatar = 23;\n  //\n  string cover_right_text = 24;\n  //\n  string desc = 25;\n  //\n  InlineProgressBar inline_progress_bar = 26;\n  //\n  SearchLikeButtonItem like_button = 27;\n  //\n  int32 official_icon = 28;\n  //\n  int32 official_icon_v2 = 29;\n  //\n  string param = 30;\n  //\n  TrafficConfig traffic_config = 31;\n  //\n  bool is_atten = 32;\n  //\n  GotoIcon goto_icon = 33;\n  //\n  bool disable_danmaku = 34;\n  //\n  bool hide_danmaku_switch = 35;\n  //\n  ReasonStyle badge_style = 36;\n  //\n  PlayerWidget player_widget = 37;\n  //\n  ReasonStyle cover_badge_style = 38;\n  //\n  RightTopLiveBadge right_top_live_badge = 39;\n}\n\n//\nmessage SearchLikeButtonItem {\n  //\n  int64 aid = 1;\n  //\n  int64 count = 2;\n  //\n  int32 selected = 3;\n  //\n  bool show_count = 4;\n  //\n  LikeResource like_resource = 5;\n  //\n  LikeResource like_night_resource = 6;\n  //\n  LikeResource dislike_resource = 7;\n  //\n  LikeResource dislike_night_resource = 8;\n}\n\n//\nmessage SearchLiveCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  RcmdReason rcmd_reason = 3;\n  //\n  string name = 4;\n  //\n  int32 online = 5;\n  //\n  string badge = 6;\n  //\n  string live_link = 7;\n  //\n  string card_left_text = 8;\n  //\n  int32 card_left_icon = 9;\n  //\n  string show_card_desc2 = 10;\n  //\n  RightTopLiveBadge right_top_live_badge = 11;\n}\n\n//\nmessage SearchLiveInlineCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  int64 mid = 3;\n  //\n  ReasonStyle rcmd_reason_style = 4;\n  //\n  int64 roomid = 5;\n  //\n  string live_link = 6;\n  //\n  SearchInlineData live_room_inline = 7;\n  //\n  string inline_type = 8;\n}\n\n//\nmessage SearchNewChannelCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  int64 id = 3;\n  //\n  string type_icon = 4;\n  //\n  ChannelLabel channel_label1 = 5;\n  //\n  ChannelLabel channel_label2 = 6;\n  //\n  ChannelLabel channel_button = 7;\n  //\n  string design_type = 8;\n  //\n  repeated ChannelMixedItem items = 9;\n}\n\n//\nmessage SearchNoResultSuggestWordCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  int32 sug_key_word_type = 3;\n}\n\n//\nmessage SearchOgvCard {\n  //\n  string title = 1;\n  //\n  string sub_title1 = 2;\n  //\n  string sub_title2 = 3;\n  //\n  string cover = 4;\n  //\n  string bg_cover = 5;\n  //\n  string special_bg_color = 6;\n  //\n  string cover_uri = 7;\n}\n\n//\nmessage SearchOgvChannelCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  int64 media_id = 3;\n  //\n  string styles = 4;\n  //\n  string area = 5;\n  //\n  string staff = 6;\n  //\n  string badge = 7;\n  //\n  WatchButton watch_button = 8;\n  //\n  double rating = 9;\n  //\n  string desc = 10;\n  //\n  repeated ReasonStyle badges_v2 = 11;\n  //\n  string styles_v2 = 12;\n}\n\n//\nmessage SearchOgvInlineCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string author = 3;\n  //\n  int32 danmaku = 4;\n  //\n  string desc = 5;\n  //\n  string face = 6;\n  //\n  string inline_type = 7;\n  //\n  int64 mid = 8;\n  //\n  int64 play = 9;\n  //\n  SearchInlineData ogv_inline = 10;\n  //\n  OgvClipInfo ogv_clip_info = 11;\n  //\n  WatchButton watch_button = 12;\n  //\n  string score = 13;\n  //\n  int32 ogv_inline_exp = 14;\n  //\n  repeated ReasonStyle badges_v2 = 15;\n}\n\n//\nmessage SearchOgvRecommendCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  repeated OgvRecommendWord items = 3;\n  //\n  string special_bg_color = 4;\n}\n\n//\nmessage SearchOgvRelationCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string special_bg_color = 3;\n  //\n  string more_text = 4;\n  //\n  string more_url = 5;\n  //\n  repeated DetailsRelationItem items = 6;\n  //\n  int32 is_new_style = 7;\n  //\n  OgvCardUI ogv_card_ui = 8;\n}\n\n//\nmessage SearchOlympicGameCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  SportsMatchItem sports_match_item = 3;\n  //\n  MatchItem match_top = 4;\n  //\n  string bg_cover = 5;\n  //\n  repeated ExtraLink extra_link = 6;\n  //\n  string inline_type = 7;\n  //\n  SearchInlineData ugc_inline = 8;\n  //\n  SearchInlineData live_room_inline = 9;\n  //\n  MatchItem match_bottom = 10;\n}\n\n//\nmessage SearchOlympicWikiCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  CardBusinessBadge card_business_badge = 3;\n  //\n  NavigationButton read_more = 4;\n  //\n  string inline_type = 5;\n  //\n  SearchInlineData ugc_inline = 6;\n  //\n  SearchInlineData live_room_inline = 7;\n  //\n  PediaCover pedia_cover = 8;\n  //\n  repeated Navigation navigation = 9;\n}\n\n//\nmessage SearchPediaCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  repeated Navigation navigation = 3;\n  //\n  NavigationButton read_more = 4;\n  //\n  int32 navigation_module_count = 5;\n  //\n  PediaCover pedia_cover = 6;\n  //\n  CardBusinessBadge card_business_badge = 7;\n}\n\n//\nmessage SearchPurchaseCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string badge = 3;\n  //\n  string venue = 4;\n  //\n  int32 price = 5;\n  //\n  string price_complete = 6;\n  //\n  int32 price_type = 7;\n  //\n  int32 required_number = 8;\n  //\n  string city = 9;\n  //\n  string show_time = 10;\n  //\n  int64 id = 11;\n  //\n  string shop_name = 12;\n}\n\n//\nmessage SearchRecommendTipCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n}\n\n//\nmessage SearchRecommendWordCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  repeated RecommendWord list = 3;\n}\n\n//\nmessage SearchSpecialCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  repeated ReasonStyle new_rec_tags = 3;\n  //\n  CardBusinessBadge card_business_badge = 4;\n  //\n  string badge = 5;\n  //\n  string desc = 6;\n  //\n  repeated ReasonStyle new_rec_tags_v2 = 7;\n}\n\n//\nmessage SearchSpecialGuideCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string phone = 3;\n  //\n  string desc = 4;\n}\n\n//\nmessage SearchSportCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string bg_cover = 3;\n  //\n  MatchItem match_top = 4;\n  //\n  MatchItem match_bottom = 5;\n  //\n  repeated ExtraLink extra_link = 6;\n  //\n  repeated MatchInfoObj items = 7;\n  //\n  int64 id = 8;\n}\n\n//\nmessage SearchSportInlineCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string bg_cover = 3;\n  //\n  MatchItem match_top = 4;\n  //\n  MatchItem match_bottom = 5;\n  //\n  repeated ExtraLink extra_link = 6;\n  //\n  repeated MatchInfoObj items = 7;\n  //\n  int64 id = 8;\n  //\n  SearchInlineData esports_inline = 9;\n  //\n  string inline_type = 10;\n}\n\n//\nmessage SearchTipsCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string sub_title = 4;\n  //\n  string cover_night = 134;\n}\n\n//\nmessage SearchTopGameCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  int32 array = 3;\n  //\n  string background_image = 4;\n  //\n  int32 button_type = 5;\n  //\n  string game_icon = 6;\n  //\n  int64 game_base_id = 7;\n  //\n  int32 game_status = 8;\n  //\n  string inline_type = 9;\n  //\n  TopGameUI top_game_ui = 10;\n  //\n  string notice_content = 11;\n  //\n  string notice_name = 12;\n  //\n  float rating = 13;\n  //\n  string score = 14;\n  //\n  repeated TabInfo tab_info = 15;\n  //\n  string tags = 16;\n  //\n  SearchInlineData ugc_inline = 17;\n  //\n  string video_cover_image = 18;\n  //\n  SearchInlineData inline_live = 19;\n}\n\n//\nmessage SearchUgcInlineCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string author = 3;\n  //\n  int32 danmaku = 4;\n  //\n  string desc = 5;\n  //\n  string inline_type = 6;\n  //\n  int64 mid = 7;\n  //\n  int64 play = 8;\n  //\n  SearchInlineData ugc_inline = 9;\n  //\n  FullTextResult full_text = 10;\n}\n\n//\nmessage SearchUpperCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  string sign = 3;\n  //\n  int32 fans = 4;\n  //\n  int32 archives = 5;\n  //\n  int32 live_status = 6;\n  //\n  int32 roomid = 7;\n  //\n  OfficialVerify official_verify = 8;\n  //\n  int32 face_nft_new = 9;\n  //\n  NftFaceIcon nft_face_icon = 10;\n  //\n  repeated AvItem av_items = 11;\n  //\n  bool is_up = 12;\n  //\n  int32 attentions = 13;\n  //\n  int32 level = 14;\n  //\n  int32 is_senior_member = 15;\n  //\n  VipInfo vip = 16;\n  //\n  Relation relation = 17;\n  //\n  string live_link = 18;\n  //\n  Notice notice = 19;\n}\n\n//\nmessage SearchVideoCard {\n  //\n  string title = 1;\n  //\n  string cover = 2;\n  //\n  RcmdReason rcmd_reason = 3;\n  //\n  repeated ReasonStyle new_rec_tags = 4;\n  //\n  repeated ThreePoint three_point = 5;\n  //\n  Share share = 6;\n  //\n  CardBusinessBadge card_business_badge = 7;\n  //\n  int64 play = 8;\n  //\n  int32 danmaku = 9;\n  //\n  string author = 10;\n  //\n  string desc = 11;\n  //\n  string duration = 12;\n  //\n  repeated ReasonStyle badges = 13;\n  //\n  int64 mid = 14;\n  //\n  string show_card_desc1 = 15;\n  //\n  string show_card_desc2 = 16;\n  //\n  FullTextResult full_text = 17;\n  //\n  repeated ReasonStyle new_rec_tags_v2 = 18;\n  //\n  repeated ReasonStyle badges_v2 = 19;\n  //\n  //Feedback feedback = 20;\n  //\n  //TimeLine time_line = 21;\n  //\n  string face = 22;\n  //\n  int32 ptime = 23;\n  //\n  string view_content = 24;\n  //\n  int32 icon_type = 25;\n  //\n  //FoldingTimeLine folding_time_line = 26;\n  //\n  //LabelStyle charging_label = 27;\n  //\n  //CardLayout card_layout = 28;\n  //\n  string author_prefix = 29;\n  //\n  repeated string highlight_tags = 30;\n  //\n  ReasonStyle cover_badge = 31;\n  //\n  //ShortOGVInfo short_ogv_info = 32;\n  //\n  int64 translation_status = 33;\n  //\n  string translated_title = 34;\n  //\n  repeated ReasonStyle quality_tags = 35;\n}\n\n//\nmessage Share {\n  //\n  string type = 1;\n  //\n  Video video = 2;\n}\n\n//\nmessage ShareButtonItem {\n  //\n  int32 type = 1;\n  //\n  repeated ButtonMeta button_metas = 2;\n}\n\n//\nmessage SharePlane {\n  //\n  string title = 1;\n  //\n  string share_subtitle = 2;\n  //\n  string desc = 3;\n  //\n  string cover = 4;\n  //\n  int64 aid = 5;\n  //\n  string bvid = 6;\n  //\n  ShareTo share_to = 7;\n  //\n  string author = 8;\n  //\n  int64 author_id = 9;\n  //\n  string short_link = 10;\n  //\n  string play_number = 11;\n  //\n  int64 room_id = 12;\n  //\n  int32 ep_id = 13;\n  //\n  string area_name = 14;\n  //\n  string author_face = 15;\n  //\n  int32 season_id = 16;\n  //\n  string share_from = 17;\n  //\n  string season_title = 18;\n  //\n  string from = 19;\n}\n\n//\nmessage ShareTo {\n  //\n  bool dynamic = 1;\n  //\n  bool im = 2;\n  //\n  bool copy = 3;\n  //\n  bool more = 4;\n  //\n  bool wechat = 5;\n  //\n  bool weibo = 6;\n  //\n  bool wechat_monment = 7;\n  //\n  bool qq = 8;\n  //\n  bool qzone = 9;\n  //\n  bool facebook = 10;\n  //\n  bool line = 11;\n  //\n  bool messenger = 12;\n  //\n  bool whats_app = 13;\n  //\n  bool twitter = 14;\n}\n\n//\nenum Sort {\n  SORT_DEFAULT = 0; //\n  SORT_VIEW_COUNT = 1; //\n  SORT_PUBLISH_TIME = 2; //\n  SORT_DANMAKU_COUNT = 3; //\n}\n\n//\nmessage Space {\n  //\n  int32 show = 1;\n  //\n  string text_color = 2;\n  //\n  string text_color_night = 3;\n  //\n  string text = 4;\n  //\n  string space_url = 5;\n}\n\n//\nmessage SportsMatchItem {\n  //\n  int64 match_id = 1;\n  //\n  int64 season_id = 2;\n  //\n  string match_name = 3;\n  //\n  string img = 4;\n  //\n  string begin_time_desc = 5;\n  //\n  string match_status_desc = 6;\n  //\n  string sub_content = 7;\n  //\n  string sub_extra_icon = 8;\n}\n\n//\nmessage Stat {\n  //\n  int64 play = 1;\n  //\n  int32 like = 2;\n  //\n  int32 reply = 3;\n}\n\n//\nmessage TabInfo {\n  //\n  string tab_name = 1;\n  //\n  string tab_url = 2;\n  //\n  int32 sort = 3;\n}\n\n//\nmessage TextButton {\n  //\n  string text = 1;\n  //\n  string uri = 2;\n}\n\n//\nmessage TextLabel {\n  //\n  string text = 1;\n  //\n  string uri = 2;\n}\n\n//\nmessage Texts {\n  //\n  string booking_text = 1;\n  //\n  string unbooking_text = 2;\n}\n\n//\nmessage ThreePoint {\n  //\n  string type = 1;\n  //\n  string icon = 2;\n  //\n  string title = 3;\n}\n\n//\nmessage ThreePoint2 {\n  //\n  repeated DislikeReason dislike_reasons = 1;\n  //\n  repeated DislikeReason feedbacks = 2;\n  //\n  int32 watch_later = 3;\n}\n\n//\nmessage ThreePointV2 {\n  //\n  string title = 1;\n  //\n  string subtitle = 2;\n  //\n  repeated DislikeReason reasons = 3;\n  //\n  string type = 4;\n  //\n  int64 id = 5;\n}\n\n//\nmessage ThreePointV3 {\n  //\n  string title = 1;\n  //\n  string selected_title = 2;\n  //\n  string subtitle = 3;\n  //\n  repeated DislikeReason reasons = 4;\n  //\n  string type = 5;\n  //\n  int64 id = 6;\n  //\n  int32 selected = 7;\n  //\n  string icon = 8;\n  //\n  string selected_icon = 9;\n  //\n  string url = 10;\n  //\n  int32 default_id = 11;\n}\n\n//\nmessage ThreePointV4 {\n  //\n  SharePlane share_plane = 1;\n  //\n  WatchLater watch_later = 2;\n}\n\n//\nmessage TopGameUI {\n  //\n  string background_image = 1;\n  //\n  string cover_default_color = 2;\n  //\n  string gaussian_blur_value = 3;\n  //\n  string mask_color_value = 4;\n  //\n  string mask_opacity = 5;\n  //\n  string module_color = 6;\n}\n\n//\nmessage TrafficConfig {\n  //\n  string title = 1;\n  //\n  repeated TrafficConfigOption options = 2;\n  //\n  int64 default_option_id = 3;\n}\n\n//\nmessage TrafficConfigOption {\n  //\n  int32 id = 1;\n  //\n  string text = 2;\n}\n\n//\nmessage UpArgs {\n  //\n  int64 up_id = 1;\n  //\n  string up_name = 2;\n  //\n  string up_face = 3;\n  //\n  int32 selected = 4;\n}\n\n//\nmessage Upper {\n  //\n  int64 mid = 1;\n  //\n  string title = 2;\n  //\n  string cover = 3;\n  //\n  string ptime_text = 4;\n}\n\n//\nenum UserSort {\n  USER_SORT_DEFAULT = 0; //\n  USER_SORT_FANS_DESCEND = 1; //\n  USER_SORT_FANS_ASCEND = 2; //\n  USER_SORT_LEVEL_DESCEND = 3; //\n  USER_SORT_LEVEL_ASCEND = 4; //\n}\n\n//\nenum UserType {\n  ALL = 0; //\n  UP = 1; //\n  NORMAL_USER = 2; //\n  AUTHENTICATED_USER = 3; //\n}\n\n//\nmessage Video {\n  //\n  string bvid = 1;\n  //\n  int64 cid = 2;\n  //\n  string share_subtitle = 3;\n  //\n  bool is_hot_label = 4;\n  //\n  int32 page = 5;\n  //\n  int32 page_count = 6;\n  //\n  string short_link = 7;\n}\n\n//\nmessage VipInfo {\n  //\n  int32 type = 1;\n  //\n  int32 status = 2;\n  //\n  int64 due_date = 3;\n  //\n  int32 vip_pay_type = 4;\n  //\n  int32 theme_type = 5;\n  //\n  VipLabel label = 6;\n  //\n  int32 avatar_subscript = 7;\n  //\n  string nickname_color = 8;\n  //\n  int64 role = 9;\n  //\n  string avatar_subscript_url = 10;\n  //\n  int32 tv_vip_status = 11;\n  //\n  int32 tv_vip_pay_type = 12;\n}\n\n//\nmessage VipLabel {\n  //\n  string path = 1;\n  //\n  string text = 2;\n  //\n  string label_theme = 3;\n  //\n  string text_color = 4;\n  //\n  int32 bg_style = 5;\n  //\n  string bg_color = 6;\n  //\n  string border_color = 7;\n  //\n  bool use_img_label = 8;\n  //\n  string img_label_uri_hans = 9;\n  //\n  string img_label_uri_hant = 10;\n  //\n  string img_label_uri_hans_static = 11;\n  //\n  string img_label_uri_hant_static = 12;\n}\n\n//\nmessage WatchButton {\n  //\n  string title = 1;\n  //\n  string link = 2;\n}\n\n//\nmessage WatchedShow {\n  //\n  bool switch = 1;\n  //\n  int64 num = 2;\n  //\n  string text_small = 3;\n  //\n  string text_large = 4;\n  //\n  string icon = 5;\n  //\n  string icon_location = 6;\n  //\n  string icon_web = 7;\n}\n\n//\nmessage WatchLater {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/polymer/community/govern/v1/govern.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.polymer.app.govern.v1;\n\noption java_multiple_files = true;\n\n// 反骚扰\nservice AntiHarassmentService {\n  //\n  rpc StoreAntiHarassmentSettings(StoreAntiHarassmentSettingsReq) returns (StoreAntiHarassmentSettingsRsp);\n  //\n  rpc LoadAntiHarassmentSettings(LoadAntiHarassmentSettingsReq) returns (LoadAntiHarassmentSettingsRsp);\n}\n\n//\nmessage AntiHarassmentInfo {\n  //\n  int32 limit = 1;\n  //\n  int32 follow_time_limit_second = 2;\n  //\n  int64 expire_time = 3;\n}\n\n//\nenum AntiHarassmentLimit {\n  DefaultLimit = 0; //\n  FollowLimit = 1; //\n  ReFollowLimit = 2; //\n  TwoWayFollow = 3; //\n  AllLimit = 4; //\n}\n\n//\nmessage AntiHarassmentSetting {\n  //\n  int64 mid = 1;\n  //\n  bool auto_limit = 2;\n  //\n  AntiHarassmentInfo im = 3;\n  //\n  AntiHarassmentInfo reply = 4;\n  //\n  AntiHarassmentInfo dm = 5;\n  //\n  AntiHarassmentInfo reply_me = 6;\n  //\n  AntiHarassmentInfo like_me = 7;\n  //\n  AntiHarassmentInfo at_me = 8;\n  //\n  int64 auto_limit_expire_time = 9;\n}\n\n//\nenum BizType {\n  InvalidBizType = 0; //\n  Im = 1; //\n  Dm = 2; //\n  Reply = 3; //\n  ReplyMe = 4; //\n  LikeMe = 5; //\n  AtMe = 6; //\n}\n\n//\nmessage LoadAntiHarassmentSettingsReq {\n  //\n  int32 biz_type = 1;\n  //\n  int64 recv_mid = 2;\n  //\n  int64 send_mid = 3;\n}\n\n//\nmessage LoadAntiHarassmentSettingsRsp {\n  //\n  bool anti_harassment_ret = 1;\n  //\n  AntiHarassmentSetting anti_harassment_setting = 2;\n  //\n  int32 show_window = 3;\n}\n\n//\nmessage StoreAntiHarassmentSettingsReq {\n  //\n  int32 biz_type = 1;\n  //\n  int64 mid = 2;\n  //\n  AntiHarassmentSetting anti_harassment_setting = 3;\n}\n\n//\nmessage StoreAntiHarassmentSettingsRsp {}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/polymer/contract/v1/contract.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.polymer.contract.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/empty.proto\";\n\n// 契约\nservice Contract {\n  //\n  rpc AddContract(AddContractReq) returns (google.protobuf.Empty);\n  //\n  rpc AddContractV2(AddContractReq) returns (AddContractReply);\n  //\n  rpc ContractConfig(ContractConfigReq) returns (ContractConfigReply);\n}\n\n//\nmessage AddContractReply {\n  //\n  bool allow_message = 1;\n  //\n  bool allow_reply = 2;\n  //\n  string input_text = 3;\n  //\n  string input_title = 4;\n}\n\n//\nmessage AddContractReq {\n  //\n  CommonReq common = 1;\n  //\n  int64 mid = 2;\n  //\n  int64 up_mid = 3;\n  //\n  int64 aid = 4;\n  //\n  int32 source = 5;\n}\n\n//\nmessage CommonReq {\n  //\n  string platform = 1;\n  //\n  int32 build = 2;\n  //\n  string buvid = 3;\n  //\n  string mobi_app = 4;\n  //\n  string device = 5;\n  //\n  string ip = 6;\n  //\n  string spmid = 7;\n}\n\n//\nmessage ContractCard {\n  //\n  string title = 1;\n  //\n  string sub_title = 2;\n}\n\n//\nmessage ContractConfigReply {\n  //\n  int32 is_follow_display = 1;\n  //\n  int32 is_triple_display = 2;\n  //\n  ContractCard contract_card = 3;\n}\n\n//\nmessage ContractConfigReq {\n  //\n  CommonReq common = 1;\n  //\n  int64 mid = 2;\n  //\n  int64 up_mid = 3;\n  //\n  int64 aid = 4;\n  //\n  int32 source = 5;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/polymer/demo/demo.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.polymer.demo;\n\noption java_multiple_files = true;\n\n//\nmessage HelloWorldReq {\n  //\n  string content = 1;\n}\n\n//\nmessage HelloWorldResp {\n  //\n  string data = 1;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/polymer/list/v1/list.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.polymer.list.v1;\n\noption java_multiple_files = true;\n\n//\nservice List {\n  //\n  rpc FavoriteTab(FavoriteTabReq) returns (FavoriteTabReply);\n  //\n  rpc CheckAccount(CheckAccountReq) returns (CheckAccountReply);\n}\n\n//\nmessage CheckAccountReply {\n  //\n  bool is_new = 1;\n}\n\n//\nmessage CheckAccountReq {\n  //\n  int64 uid = 1;\n  //\n  string periods = 2;\n}\n\n//\nmessage FavoriteTabItem {\n  //\n  string name = 1;\n  //\n  string uri = 2;\n  //\n  string type = 3;\n}\n\n//\nmessage FavoriteTabReply {\n  //\n  repeated FavoriteTabItem items = 1;\n}\n\n//\nmessage FavoriteTabReq {}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/relation/interfaces/api.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.relation.interface.v1;\n\noption java_multiple_files = true;\noption java_package = \"bilibili.relation.interface1.v1\";\n\nservice RelationInterface {\n  // 评论区 At 用户列表 (无需登录鉴权)\n  rpc AtSearch (AtSearchReq) returns (AtSearchReply);\n}\n\nmessage AtSearchReq {\n  // 可以为 1 , 但是不能为 0 或空 不知道有啥用\n  int64 mid = 1;\n  // 用户名搜索关键词\n  string keyword = 2;\n}\n\nmessage AtSearchReply {\n  // 搜索结果分组\n  repeated AtGroup items = 1;\n}\n\nmessage AtGroup {\n  // 分组类型  2: 我的关注 4:其他 ,其他自测\n  int32 group_type = 1;\n  // 分组名称\n  string group_name = 2;\n  // 用户列表\n  repeated AtItem items = 3;\n}\n\nmessage AtItem {\n  int64 mid = 1;\n  string name = 2;\n  string face = 3;\n  int32 fans = 4;\n  int32 official_verify_type = 5;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/render/render.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.render;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/any.proto\";\n\n// \nmessage Render {\n  //\n  int64 code = 1;\n  //\n  string message = 2;\n  //\n  string ttl = 3;\n  //\n  google.protobuf.Any data = 4;\n}"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/rpc/status.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.rpc;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/any.proto\";\n\n// 响应gRPC Status\n// 当status code是[UNKNOWN = 2]时，details为业务详细的错误信息，进行proto any转换成业务码结构体\nmessage Status {\n  // 业务错误码\n  int32 code = 1;\n  // 业务错误信息\n  string message = 2;\n  // 扩展信息嵌套(相当于该messasge的套娃)\n  repeated google.protobuf.Any details = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/tv/interfaces/dm/v1/dm.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.tv.interfaces.dm.v1;\n\noption java_multiple_files = true;\noption java_package = \"bilibili.tv.interfaces.dm.v1\";\n\n//\nmessage Chronos {\n  //\n  string md5 = 1;\n  //\n  string file = 2;\n  //\n  string sign = 3;\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  int64 display = 11;\n}\n\n// ott互动弹幕条目信息\nmessage CommandDmOtt {\n  // 弹幕id\n  int64 id = 1;\n  // 对象视频cid\n  int64 oid = 2;\n  // 发送者mid\n  int64 mid = 3;\n  //\n  int32 type = 4;\n  // 互动弹幕指令\n  string command = 5;\n  // 互动弹幕正文\n  string content = 6;\n  //\n  int32 state = 7;\n  // 出现时间\n  int32 progress = 8;\n  // 创建时间\n  string ctime = 9;\n  // 发布时间\n  string mtime = 10;\n  // 扩展json数据\n  string extra = 11;\n  // 弹幕id str类型\n  string id_str = 12;\n}\n\n//\nmessage CommandDmsOttReply {\n  //\n  repeated CommandDmOtt command_dms_ott = 1;\n}\n\n//\nmessage CommandDmsOttReq {\n  //\n  int64 aid = 1;\n  //\n  int64 cid = 2;\n  //\n  int64 mid = 3;\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  // 弹幕类型\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  // 弹幕池\n  int32 pool = 11;\n  // 弹幕dmid str\n  string idStr = 12;\n  // 弹幕属性位(bin求AND)\n  // bit0:保护 bit1:直播 bit2:高赞\n  int32 attr = 13;\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}\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}\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\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}\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 from = 6;\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  //\n  bool command_close = 13;\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  int64 from = 6;\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\n//\nmessage TvViewProgressReply {\n  //\n  VideoGuide video_guide = 1;\n  //\n  Chronos chronos = 2;\n}\n\n//\nmessage TvViewProgressReq {\n  //\n  int64 aid = 1;\n  //\n  int64 cid = 2;\n  //\n  int64 up_mid = 3;\n  //\n  string engine_version = 4;\n  //\n  string message_protocol = 5;\n  //\n  string service_key = 6;\n  //\n  int64 sid = 7;\n  //\n  int64 pid = 8;\n  //\n  int64 from = 9;\n  //\n  string guest_access_key = 10;\n  //\n  int64 epid = 11;\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 VideoGuide {\n  //\n  repeated CommandDm command_dms = 2;\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}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/vega/deneb/v1/deneb.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.vega.deneb.v1;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/any.proto\";\n\n//\nservice VegaDenebRPC {\n  //\n  rpc MessagePulls (MessagePullsReq) returns (MessagePullsReply);\n}\n\n//\nmessage MessagePullsReply {\n  //\n  repeated google.protobuf.Any data = 1;\n  //\n  int32 pn = 2;\n  //\n  int32 ps = 3;\n  //\n  int64 count = 4;\n  //\n  bool has_next = 5;\n}\n\n//\nmessage MessagePullsReq {\n  //\n  int64 start_seq_id = 1;\n  //\n  int64 end_seq_id = 2;\n  //\n  int32 pn = 3;\n  //\n  int32 ps = 4;\n}\n\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/web/interfaces/v1/interfaces.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.web.interfaces.v1;\n\noption java_multiple_files = true;\noption java_package = \"bilibili.web.interfaces.v1\";\n\n// 用户信息\nmessage AccInfo {\n  // 用户UID\n  int64 mid = 1;\n  // 用户昵称\n  string name = 2;\n  //\n  string sex = 3;\n  //\n  string face = 4;\n  //\n  string sign = 5;\n}\n\n//\nmessage AccountCard {\n  //\n  string mid = 1;\n  //\n  string name = 2;\n  //\n  string sex = 3;\n  //\n  string rank = 4;\n  //\n  string face = 5;\n  //\n  int32 spacesta = 6;\n  //\n  string sign = 7;\n  //\n  CardLevelInfo level_info = 8;\n  //\n  PendantInfo pendant = 9;\n  //\n  NameplateInfo nameplate = 10;\n  //\n  OfficialInfo official = 11;\n  //\n  OfficialVerify official_verify = 12;\n  //\n  CardVip vip = 13;\n  //\n  int64 fans = 14;\n  //\n  int64 friend = 15;\n  //\n  int64 attention = 16;\n}\n\n//\nmessage ActivityArchiveReply {\n  //\n  Arc arc = 1;\n  //\n  string bvid = 2;\n  //\n  repeated Page pages = 3;\n  //\n  ReqUser req_user = 4;\n  //\n  repeated Staff staff = 5;\n  //\n  OperationRelate right_relate = 6;\n  //\n  OperationRelate bottom_relate = 7;\n}\n\n//\nmessage ActivityArchiveReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n  //\n  string activity_key = 3;\n}\n\n//\nmessage ActivityEpisode {\n  //\n  int64 id = 1;\n  //\n  int64 aid = 2;\n  //\n  string bvid = 3;\n  //\n  int64 cid = 4;\n  //\n  string title = 5;\n  //\n  string cover = 6;\n  //\n  Author author = 7;\n  //\n  Rights rights = 8;\n}\n\n//\nmessage ActivityGame {\n  //\n  repeated ActivityGameIframe iframes = 1;\n  //\n  string disclaimer = 2;\n  //\n  string disclaimer_url = 3;\n}\n\n//\nmessage ActivityGameIframe {\n  //\n  string url = 1;\n  //\n  int64 height = 2;\n}\n\n//\nmessage ActivityLive {\n  //\n  int64 room_id = 1;\n  //\n  int64 now_time = 2;\n  //\n  int64 start_time = 3;\n  //\n  int64 end_time = 4;\n  //\n  string hover_pic = 5;\n  //\n  string hover_jump_url = 6;\n  //\n  int64 break_cycle = 7;\n  //\n  repeated LiveTimeline timeline = 8;\n  //\n  OperationRelate operation_relate = 9;\n  //\n  int64 reply_type = 10;\n  //\n  int64 reply_id = 11;\n  //\n  string hover_pic_close = 12;\n  //\n  string gift_disclaimer = 13;\n}\n\n//\nmessage ActivityLiveTimeInfoReply {\n  //\n  int64 now_time = 1;\n  //\n  int64 start_time = 2;\n  //\n  int64 end_time = 3;\n  //\n  repeated LiveTimeline timeline = 4;\n}\n\n//\nmessage ActivityLiveTimeInfoReq {\n  //\n  string activity_key = 1;\n}\n\n//\nmessage ActivitySeasonReply {\n  //\n  ActivitySeasonStatus status = 1;\n  //\n  string title = 2;\n  //\n  ActivityLive live = 3;\n  //\n  ActivitySubscribe subscribe = 4;\n  //\n  ActivityGame game = 5;\n  //\n  ActivityView view = 6;\n  //\n  ActivityTheme theme = 7;\n}\n\n//\nmessage ActivitySeasonReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n  //\n  string activity_key = 3;\n}\n\n//\nmessage ActivitySeasonSection {\n  //\n  int64 id = 1;\n  //\n  string title = 2;\n  //\n  int64 type = 3;\n  //\n  repeated ActivityEpisode episodes = 4;\n}\n\n//\nenum ActivitySeasonStatus {\n  StatusNone = 0; //\n  StatusLive = 1; //\n  StatusView = 2; //\n}\n\n//\nmessage ActivitySubscribe {\n  //\n  bool status = 1;\n  //\n  string title = 2;\n  //\n  string button_title = 3;\n  //\n  string button_selected_title = 4;\n  //\n  int64 season_stat_view = 5;\n  //\n  int64 season_stat_danmaku = 6;\n  //\n  OrderType order_type = 7;\n  oneof param {\n    //\n    ReserveActivityParam reserve_activity_param = 8;\n    //\n    FavSeasonParam fav_season_param = 9;\n    //\n    JumpURLParam jump_URL_param = 10;\n  }\n}\n\n//\nmessage ActivityTheme {\n  //\n  string base_color = 1;\n  //\n  string loading_bg_color = 2;\n  //\n  string operated_bg_color = 3;\n  //\n  string default_element_color = 4;\n  //\n  string hover_element_color = 5;\n  //\n  string selected_element_color = 6;\n  //\n  string base_font_color = 7;\n  //\n  string info_font_color = 8;\n  //\n  string mask_bg_color = 9;\n  //\n  string page_bg_color = 10;\n  //\n  string center_logo_img = 11;\n  //\n  string page_bg_img = 12;\n  //\n  string decorations2233_img = 13;\n  //\n  string main_banner_bg_img = 14;\n  //\n  string main_banner_title_img = 15;\n  //\n  string like_animation_img = 16;\n  //\n  string combo_like_img = 17;\n  //\n  string combo_coin_img = 18;\n  //\n  string combo_fav_img = 19;\n  //\n  string arrow_btn_img = 20;\n  //\n  string share_icon_bg_img = 21;\n  //\n  string live_list_location_img = 22;\n  //\n  string live_list_location_img_active = 23;\n  //\n  string player_loading_img = 24;\n  //\n  string share_img = 25;\n  //\n  map<string, string> kv_color = 26;\n}\n\n//\nmessage ActivityView {\n  //\n  Arc arc = 1;\n  //\n  string bvid = 2;\n  //\n  repeated Page pages = 3;\n  //\n  repeated Staff staff = 4;\n  //\n  ReqUser req_user = 5;\n  //\n  OperationRelate right_relate = 6;\n  //\n  OperationRelate bottom_relate = 7;\n  //\n  repeated ActivitySeasonSection sections = 8;\n}\n\n//\nmessage Arc {\n  //\n  int64 aid = 1;\n  //\n  int64 videos = 2;\n  //\n  int32 type_id = 3;\n  //\n  string type_name = 4;\n  //\n  int32 copyright = 5;\n  //\n  string pic = 6;\n  //\n  string title = 7;\n  //\n  int64 pubdate = 8;\n  //\n  int64 ctime = 9;\n  //\n  string desc = 10;\n  //\n  int32 state = 11;\n  //\n  int32 access = 12;\n  //\n  int32 attribute = 13;\n  //\n  string tag = 14;\n  //\n  repeated string tags = 15;\n  //\n  int64 duration = 16;\n  //\n  int64 mission_id = 17;\n  //\n  int64 order_id = 18;\n  //\n  string redirect_url = 19;\n  //\n  int64 forward = 20;\n  //\n  Rights rights = 21;\n  //\n  Author author = 22;\n  //\n  Stat stat = 23;\n  //\n  string report_result = 24;\n  //\n  string dynamic = 25;\n  //\n  int64 first_cid = 26;\n  //\n  Dimension dimension = 27;\n  //\n  repeated StaffInfo staff_info = 28;\n  //\n  int64 season_id = 29;\n  //\n  repeated DescV2 desc_v2 = 30;\n  //\n  bool is_chargeable_season = 31;\n  //\n  bool is_blooper = 32;\n}\n\n//\nmessage Author {\n  //\n  int64 mid = 1;\n  //\n  string name = 2;\n  //\n  string face = 3;\n}\n\n//\nmessage Card {\n  //\n  AccountCard card = 1;\n  //\n  Space space = 2;\n  //\n  bool following = 3;\n  //\n  int64 archive_count = 4;\n  //\n  int32 article_count = 5;\n  //\n  int64 follower = 6;\n}\n\n//\nmessage CardLevelInfo {\n  //\n  int32 cur = 1;\n  //\n  int32 min = 2;\n  //\n  int32 now_exp = 3;\n  //\n  int32 next_exp = 4;\n}\n\n//\nmessage CardVip {\n  //\n  int32 type = 1;\n  //\n  string due_remark = 2;\n  //\n  int32 access_status = 3;\n  //\n  int32 vip_status = 4;\n  //\n  string vip_status_warn = 5;\n  //\n  int32 theme_type = 6;\n}\n\nmessage ClickActivitySeasonReq {\n  //\n  OrderType order_type = 1;\n  oneof param {\n    //\n    ReserveActivityParam reserve_activity_param = 2;\n    //\n    FavSeasonParam fav_season_param = 3;\n    //\n    JumpURLParam jump_URL_param = 4;\n  }\n  //\n  string spmid = 5;\n  //\n  int64 action = 6;\n}\n\n//\nmessage DescV2 {\n  //\n  string raw_text = 1;\n  //\n  int64 type = 2;\n  //\n  int64 biz_id = 3;\n}\n\n//\nmessage Dimension {\n  //\n  int64 width = 1;\n  //\n  int64 height = 2;\n  //\n  int64 rotate = 3;\n}\n\n//\nmessage FavSeasonParam {\n  //\n  int64 season_id = 1;\n}\n\n//\nmessage HotReply {\n  //\n  ReplyPage page = 1;\n  //\n  repeated Reply replies = 2;\n}\n\n//\nmessage JumpURLParam {\n  //\n  string jump_url = 1;\n}\n\n//\nmessage LiveTimeline {\n  //\n  string name = 1;\n  //\n  int64 start_time = 2;\n  //\n  int64 end_time = 3;\n  //\n  string cover = 4;\n  //\n  string subtitle = 5;\n  //\n  string h5_cover = 6;\n}\n\n//\nmessage NameplateInfo {\n  //\n  int32 nid = 1;\n  //\n  string name = 2;\n  //\n  string image = 3;\n  //\n  string image_small = 4;\n  //\n  string level = 5;\n  //\n  string condition = 6;\n}\n\n//\nmessage NoReply {}\n\n//\nmessage OfficialInfo {\n  //\n  int32 role = 1;\n  //\n  string title = 2;\n  //\n  string desc = 3;\n}\n\n//\nmessage OfficialVerify {\n  //\n  int32 type = 1;\n  //\n  string desc = 2;\n}\n\n//\nmessage OperationRelate {\n  //\n  string title = 1;\n  //\n  repeated RelateItem relate_item = 2;\n  //\n  repeated Relate ai_relate_item = 3;\n}\n\n//\nenum OrderType {\n  TypeNone = 0; //\n  TypeOrderActivity = 1; //\n  TypeFavSeason = 2; //\n  TypeClick = 3; //\n}\n\n//\nmessage Page {\n  //\n  int64 cid = 1;\n  //\n  int32 page = 2;\n  //\n  string from = 3;\n  //\n  string part = 4;\n  //\n  int64 duration = 5;\n  //\n  string vid = 6;\n  //\n  string desc = 7;\n  //\n  string weblink = 8;\n  //\n  Dimension dimension = 9;\n}\n\n//\nmessage PendantInfo {\n  //\n  int32 pid = 1;\n  //\n  string name = 2;\n  //\n  string image = 3;\n  //\n  int64 expire = 4;\n}\n\n//\nmessage ReasonStyle {\n  //\n  string text = 1;\n}\n\n//\nmessage Relate {\n  //\n  Arc arc = 1;\n  //\n  string bvid = 2;\n  //\n  int64 season_type = 3;\n}\n\n//\nmessage RelateItem {\n  //\n  string url = 1;\n  //\n  string cover = 2;\n}\n\n//\nmessage Relation {\n  //\n  int64 attribute = 1;\n  //\n  int64 special = 3;\n}\n\n//\nmessage Reply {\n  //\n  int64 rpid = 1;\n  //\n  int64 oid = 2;\n  //\n  int32 type = 3;\n  //\n  int64 mid = 4;\n  //\n  int64 root = 5;\n  //\n  int64 parent = 6;\n  //\n  int64 dialog = 7;\n  //\n  int32 count = 8;\n  //\n  int32 rcount = 9;\n  //\n  int32 floor = 10;\n  //\n  int32 state = 11;\n  //\n  int32 fans_grade = 12;\n  //\n  int32 attr = 13;\n  //\n  int64 ctime = 14;\n  //\n  string rpid_str = 15;\n  //\n  string root_str = 16;\n  //\n  string parent_str = 17;\n  //\n  string dialog_str = 18;\n  //\n  int32 like = 19;\n  //\n  int32 hate = 20;\n  //\n  int32 action = 21;\n  //\n  ReplyMember member = 22;\n  //\n  ReplyContent content = 23;\n  //\n  repeated Reply replies = 24;\n  //\n  int32 assist = 25;\n  //\n  ReplyFolder folder = 26;\n  //\n  ReplyUpAction up_action = 27;\n  //\n  ReplyLabel label = 28;\n  //\n  string raw_input = 29;\n  //\n  bool show_follow = 30;\n}\n\n//\nmessage ReplyContent {\n  //\n  int64 rp_id = 1;\n  //\n  string message = 2;\n  //\n  ReplyVote vote = 3;\n  //\n  repeated string topics = 5;\n  //\n  int32 ip = 6;\n  //\n  int32 plat = 7;\n  //\n  string device = 8;\n  //\n  string version = 9;\n  //\n  repeated ReplyMemberInfo members = 10;\n  //\n  map<string, ReplyEmote> emote = 11;\n}\n\n//\nmessage ReplyEmote {\n  //\n  int64 id = 1;\n  //\n  int64 package_id = 2;\n  //\n  int64 state = 3;\n  //\n  int64 type = 4;\n  //\n  int64 attr = 5;\n  //\n  string text = 6;\n  //\n  string url = 7;\n  //\n  ReplyEmoteMeta meta = 8;\n  //\n  int64 ctime = 9;\n  //\n  int64 mtime = 10;\n}\n\n//\nmessage ReplyEmoteMeta {\n  //\n  ReplyEmoteMetaSize size = 1;\n}\n\n//\nenum ReplyEmoteMetaSize {\n  EMOTE_META_SIZE_UNSPECIFIED = 0; //\n  EMOTE_META_SIZE_SMALL = 1; //\n  EMOTE_META_SIZE_BIG = 2; //\n}\n\n//\nmessage ReplyFansDetail {\n  //\n  int64 uid = 1;\n  //\n  int32 medal_id = 2;\n  //\n  string medal_name = 3;\n  //\n  int32 score = 4;\n  //\n  int32 level = 5;\n  //\n  int32 intimacy = 6;\n  //\n  int32 status = 7;\n  //\n  int32 received = 8;\n}\n\n//\nmessage ReplyFolder {\n  //\n  bool has_folded = 1;\n  //\n  bool is_folded = 2;\n  //\n  string rule = 3;\n}\n\n//\nmessage ReplyLabel {\n  //\n  int64 rpid = 1;\n  //\n  string content = 2;\n  //\n  string text_color = 3;\n  //\n  string text_color_night_mode = 4;\n  //\n  string bg_color = 5;\n  //\n  string bg_color_night_mode = 6;\n  //\n  string link = 7;\n  //\n  string position = 8;\n}\n\n//\nmessage ReplyLevelInfo {\n  //\n  int32 cur = 1;\n  //\n  int32 min = 2;\n  //\n  int32 now_exp = 3;\n  //\n  int32 next_exp = 4;\n}\n\n//\nmessage ReplyMember {\n  //\n  ReplyMemberInfo info = 1;\n  //\n  ReplyFansDetail fans_detail = 2;\n  //\n  int32 following = 3;\n}\n\n//\nmessage ReplyMemberInfo {\n  //\n  int32 role = 1;\n  //\n  string mid = 2;\n  //\n  string name = 3;\n  //\n  string sex = 4;\n  //\n  string sign = 5;\n  //\n  string avatar = 6;\n  //\n  string rank = 7;\n  //\n  string display_rank = 8;\n  //\n  ReplyLevelInfo level_info = 9;\n  //\n  PendantInfo pendant = 10;\n  //\n  NameplateInfo nameplate = 11;\n  //\n  OfficialVerify official_verify = 12;\n  //\n  ReplyVip vip = 13;\n}\n\n//\nmessage ReplyPage {\n  //\n  int64 acount = 1;\n  //\n  int64 count = 2;\n  //\n  int64 num = 3;\n  //\n  int64 size = 4;\n}\n\n//\nmessage ReplyUpAction {\n  //\n  bool like = 1;\n  //\n  bool reply = 2;\n}\n\n//\nmessage ReplyVip {\n  //\n  int32 type = 1;\n  //\n  int64 due_date = 2;\n  //\n  string due_remark = 3;\n  //\n  int32 access_status = 4;\n  //\n  int32 vip_status = 5;\n  //\n  string vip_status_warn = 6;\n  //\n  int32 theme_type = 7;\n  //\n  VipLabel label = 8;\n}\n\n//\nmessage ReplyVote {\n  //\n  int64 id = 1;\n  //\n  string title = 2;\n  //\n  int32 cnt = 3;\n  //\n  string desc = 4;\n  //\n  bool deleted = 5;\n}\n\n//\nmessage ReqUser {\n  //\n  bool favorite = 1;\n  //\n  bool like = 2;\n  //\n  bool dislike = 3;\n  //\n  int64 multiply = 4;\n}\n\n//\nmessage ReserveActivityParam {\n  //\n  int64 reserve_id = 1;\n  //\n  string from = 2;\n  //\n  string type = 3;\n  //\n  int64 oid = 4;\n}\n\n//\nmessage Rights {\n  //\n  int32 bp = 1;\n  //\n  int32 elec = 2;\n  //\n  int32 download = 3;\n  //\n  int32 movie = 4;\n  //\n  int32 pay = 5;\n  //\n  int32 hd5 = 6;\n  //\n  int32 no_reprint = 7;\n  //\n  int32 autoplay = 8;\n  //\n  int32 ugc_pay = 9;\n  //\n  int32 is_cooperation = 10;\n  //\n  int32 ugc_pay_preview = 11;\n  //\n  int32 arc_pay = 12;\n  //\n  int32 free_watch = 13;\n}\n\n//\nmessage SeasonEpisode {\n  //\n  int64 season_id = 1;\n  //\n  int64 section_id = 2;\n  //\n  int64 id = 3;\n  //\n  int64 aid = 4;\n  //\n  int64 cid = 5;\n  //\n  string title = 6;\n  //\n  int64 attribute = 7;\n  //\n  Arc arc = 8;\n  //\n  Page page = 9;\n  //\n  string bvid = 10;\n  //\n  ReasonStyle badge_style = 11;\n}\n\n//\nmessage SeasonSection {\n  //\n  int64 season_id = 1;\n  //\n  int64 id = 2;\n  //\n  string title = 3;\n  //\n  int64 type = 4;\n  //\n  repeated SeasonEpisode episodes = 5;\n}\n\n//\nmessage SeasonStat {\n  //\n  int64 season_id = 1;\n  //\n  int64 view = 2;\n  //\n  int32 danmaku = 3;\n  //\n  int32 reply = 4;\n  //\n  int32 fav = 5;\n  //\n  int32 coin = 6;\n  //\n  int32 share = 7;\n  //\n  int32 now_rank = 8;\n  //\n  int32 his_rank = 9;\n  //\n  int32 like = 10;\n}\n\n//\nmessage Space {\n  //\n  string s_img = 1;\n  //\n  string l_img = 2;\n}\n\n//\nmessage Staff {\n  //\n  int64 mid = 1;\n  //\n  string title = 2;\n  //\n  string name = 3;\n  //\n  string face = 4;\n  //\n  VipInfo vip = 5;\n  //\n  OfficialInfo official = 6;\n  //\n  int64 follower = 7;\n  //\n  int32 label_style = 8;\n  //\n  Relation relation = 9;\n}\n\n//\nmessage StaffInfo {\n  //\n  int64 mid = 1;\n  //\n  string title = 2;\n}\n\n//\nmessage Stat {\n  //\n  int64 aid = 1;\n  //\n  int64 view = 2;\n  //\n  int32 danmaku = 3;\n  //\n  int32 reply = 4;\n  //\n  int32 fav = 5;\n  //\n  int32 coin = 6;\n  //\n  int32 share = 7;\n  //\n  int32 now_rank = 8;\n  //\n  int32 his_rank = 9;\n  //\n  int32 like = 10;\n  //\n  int32 dislike = 11;\n  //\n  string evaluation = 12;\n  //\n  string argue_msg = 13;\n}\n\n//\nmessage Subtitle {\n  //\n  bool allow_submit = 1;\n  //\n  repeated SubtitleItem list = 2;\n}\n\n//\nmessage SubtitleItem {\n  //\n  int64 id = 1;\n  //\n  string lan = 2;\n  //\n  string lan_doc = 3;\n  //\n  bool is_lock = 4;\n  //\n  int64 author_mid = 5;\n  //\n  string subtitle_url = 6;\n  //\n  AccInfo author = 7;\n}\n\n//\nmessage Tag {\n  //\n  int64 id = 1;\n  //\n  string name = 2;\n  //\n  string cover = 3;\n  //\n  string head_cover = 4;\n  //\n  string content = 5;\n  //\n  string short_content = 6;\n  //\n  int32 type = 7;\n  //\n  int32 state = 8;\n  //\n  int64 ctime = 9;\n  //\n  TagCount tag_count = 10;\n  //\n  int32 is_atten = 11;\n  //\n  int64 likes = 12;\n  //\n  int64 hates = 13;\n  //\n  int32 attribute = 14;\n  //\n  int32 liked = 15;\n  //\n  int32 hated = 16;\n}\n\n//\nmessage TagCount {\n  //\n  int64 view = 1;\n  //\n  int64 use = 2;\n  //\n  int64 atten = 3;\n}\n\n//\nmessage UGCPayAsset {\n  //\n  int64 price = 1;\n  //\n  map<string, int64> platform_price = 2;\n}\n\n//\nmessage UGCSeason {\n  //\n  int64 id = 1;\n  //\n  string title = 2;\n  //\n  string cover = 3;\n  //\n  int64 mid = 4;\n  //\n  string intro = 5;\n  //\n  int32 sign_state = 6;\n  //\n  int64 attribute = 7;\n  //\n  repeated SeasonSection sections = 8;\n  //\n  SeasonStat stat = 9;\n  //\n  int64 ep_count = 10;\n  //\n  int64 season_type = 11;\n  //\n  bool is_pay_season = 12;\n}\n\n//\nmessage View {\n  //\n  Arc arc = 1;\n  //\n  bool no_cache = 2;\n  //\n  repeated Page pages = 3;\n  //\n  Subtitle subtitle = 4;\n  //\n  UGCPayAsset asset = 5;\n  //\n  ViewLabel label = 6;\n  //\n  repeated Staff staff = 7;\n  //\n  UGCSeason ugc_season = 8;\n  //\n  int64 stein_guide_cid = 9;\n}\n\n//\nmessage ViewDetailReply {\n  //\n  View view = 1;\n  //\n  Card card = 2;\n  //\n  repeated Tag tags = 3;\n  //\n  HotReply reply = 4;\n  //\n  repeated Arc related = 5;\n}\n\n//\nmessage ViewDetailReq {\n  //\n  int64 aid = 1;\n  //\n  string bvid = 2;\n}\n\n//\nmessage ViewLabel {\n  //\n  int64 type = 1;\n}\n\n//\nmessage VipInfo {\n  //\n  int32 type = 1;\n  //\n  int32 status = 2;\n  //\n  int32 vip_pay_type = 3;\n  //\n  int32 theme_type = 4;\n}\n\n//\nmessage VipLabel {\n  //\n  string path = 1;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/bilibili/web/space/v1/space.proto",
    "content": "syntax = \"proto3\";\n\npackage bilibili.web.space.v1;\n\noption java_multiple_files = true;\n\n//\nmessage NoReply {}\n\n//\nmessage OfficialReply {\n  //\n  int64 id = 1;\n  //\n  int64 uid = 2;\n  //\n  string name = 3;\n  //\n  string icon = 4;\n  //\n  string scheme = 5;\n  //\n  string rcmd = 6;\n  //\n  string ios_url = 7;\n  //\n  string android_url = 8;\n  //\n  string button = 9;\n  //\n  string deleted = 10;\n  //\n  int64 mtime = 11;\n}\n\n//\nmessage OfficialRequest {\n  //\n  int64 mid = 1;\n}\n\n//\nmessage PhotoMall {\n  //\n  int64 id = 1;\n  //\n  string name = 2;\n  //\n  string img = 3;\n  //\n  string night_img = 4;\n  //\n  int64 is_activated = 5;\n}\n\n//\nmessage PhotoMallListReply {\n  //\n  repeated PhotoMall list = 1;\n}\n\n//\nmessage PhotoMallListReq {\n  //\n  string mobiapp = 1;\n  //\n  int64 mid = 2;\n  //\n  string device = 3;\n}\n\n//\nmessage PrivacyReply {\n  //\n  map<string, int64> privacy = 1;\n}\n\n//\nmessage PrivacyRequest {\n  //\n  int64 mid = 1;\n}\n\n//\nmessage SetTopPhotoReq {\n  //\n  string mobiapp = 1;\n  //\n  int64 i_d = 2;\n  //\n  int64 mid = 3;\n  //\n  int32 type = 4;\n}\n\n//\nmessage SpaceSettingReply {\n  //\n  int64 channel = 1;\n  //\n  int64 fav_video = 2;\n  //\n  int64 coins_video = 3;\n  //\n  int64 likes_video = 4;\n  //\n  int64 bangumi = 5;\n  //\n  int64 played_game = 6;\n  //\n  int64 groups = 7;\n  //\n  int64 comic = 8;\n  //\n  int64 b_bq = 9;\n  //\n  int64 dress_up = 10;\n  //\n  int64 disable_following = 11;\n  //\n  int64 live_playback = 12;\n  //\n  int64 close_space_medal = 13;\n  //\n  int64 only_show_wearing = 14;\n  //\n  int64 disable_show_school = 15;\n  //\n  int64 disable_show_nft = 16;\n}\n\n//\nmessage SpaceSettingReq {\n  //\n  int64 mid = 1;\n}\n\n//\nmessage TopPhoto {\n  //\n  string img_url = 1;\n  //\n  string night_img_url = 2;\n  //\n  int64 sid = 3;\n}\n\n//\nmessage TopPhotoArc {\n  //\n  bool show = 1;\n  //\n  int64 aid = 2;\n  //\n  string pic = 3;\n}\n\n//\nmessage TopPhotoArcCancelReq {\n  //\n  int64 mid = 1;\n}\n\n//\nmessage TopPhotoReply {\n  //\n  TopPhoto top_photo = 1;\n  //\n  TopPhotoArc top_photo_arc = 2;\n}\n\n//\nmessage TopPhotoReq {\n  //\n  string mobiapp = 1;\n  //\n  int64 mid = 2;\n  //\n  int32 build = 3;\n  //\n  string device = 4;\n  //\n  int64 login_mid = 5;\n}\n\n//\nenum TopPhotoType {\n  UNKNOWN = 0; //\n  PIC = 1; //\n  ARCHIVE = 2; //\n}\n\n//\nmessage UpActivityTabReq {\n  //\n  int64 mid = 1;\n  //\n  int32 state = 2;\n  //\n  int64 tab_cont = 3;\n  //\n  string tab_name = 4;\n}\n\n//\nmessage UpActivityTabResp {\n  //\n  bool success = 1;\n}\n\n//\nmessage UpRcmdBlackListReply {}\n\n//\nmessage UserTabReply {\n  //\n  int32 tab_type = 1;\n  //\n  int64 mid = 2;\n  //\n  string tab_name = 3;\n  //\n  int32 tab_order = 4;\n  //\n  int64 tab_cont = 5;\n  //\n  int32 is_default = 6;\n  //\n  string h5_link = 7;\n}\n\n//\nmessage UserTabReq {\n  //\n  int64 mid = 1;\n  //\n  int32 plat = 2;\n  //\n  int32 build = 3;\n}\n\n//\nmessage WhitelistAddReply {\n  //\n  bool add_ok = 1;\n}\n\n//\nmessage WhitelistAddReq {\n  //\n  int64 mid = 1;\n  //\n  int64 stime = 2;\n  //\n  int64 etime = 3;\n}\n\n//\nmessage WhitelistReply {\n  //\n  bool is_white = 1;\n}\n\n//\nmessage WhitelistReq {\n  //\n  int64 mid = 1;\n}\n\n//\nmessage WhitelistUpReply {\n  //\n  bool up_ok = 1;\n}\n\n//\nmessage WhitelistValidTimeReply {\n  //\n  bool is_white = 1;\n  //\n  int64 stime = 2;\n  //\n  int64 etime = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/common/ErrorProto.proto",
    "content": "syntax = \"proto3\";\n\npackage common;\n\noption java_multiple_files = true;\n\nimport \"google/protobuf/any.proto\";\n\n// 响应gRPC Status\n// 当status code是[UNKNOWN = 2]时，details为业务详细的错误信息，进行proto any转换成业务码结构体\nmessage ErrorProto {\n  // 业务错误码\n  int32 code = 1;\n  // 业务错误信息\n  string message = 2;\n  // 扩展信息嵌套(相当于该messasge的套娃)\n  repeated google.protobuf.Any details = 3;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/datacenter/hakase/protobuf/android_device_info.proto",
    "content": "syntax = \"proto3\";\n\npackage datacenter.hakase.protobuf;\n\noption java_multiple_files = true;\n\nmessage AndroidDeviceInfo {\n  // ?\n  string sdkver = 1;\n  // 产品id\n  // 粉 白 蓝 直播姬 HD 海外 OTT 漫画 TV野版 小视频 网易漫画 网易漫画 网易漫画HD 国际版 东南亚版\n  // 1  2  3    4    5   6    7   8     9     10      11       12       13       14       30\n  string app_id = 2;\n  // 版本号, 如 \"7.39.0\"\n  string app_version = 3;\n  // 版本号, 如 \"7390300\"\n  string app_version_code = 4;\n  // 用户 mid\n  string mid = 5;\n  // 渠道号, 如 \"master\"\n  string chid = 6;\n  // APP 首次安装启动时间戳\n  int64 fts = 7;\n  // 此处实际为 fp, 但不知为何命名为 buvid_local\n  string buvid_local = 8;\n  // 留空为 0\n  int32 first = 9;\n  // 进程名, 如 \"tv.danmaku.bili\"\n  string proc = 10;\n  // 网络信息, 为一数组直接 toString() 的结果\n  // 如 \"\"\"[\"dummy0,fe80::18d8:6ff:fe46:c2ba%dummy0,\", \"wlan0,fe80::a0f4:6dff:fea8:2d37%wlan0,192.168.1.5,\", \"lo,::1,127.0.0.1,\", \"rmnet_ims00,fe80::5a02:3ff:fe04:512%rmnet_ims00,2409:815a:7c38:cee1:1773:d0b9:d163:b023,\"]\"\"\"\n  string net = 11;\n  // 手机无线电固件版本号(`Build.getRadioVersion()`). 如 `21C20B686S000C000,21C20B686S000C000`.\n  string band = 12;\n  // OS 版本号, 如 \"12\"\n  string osver = 13;\n  // 当前毫秒时间戳\n  int64 t = 14;\n  // CPU 逻辑核心计数\n  int32 cpu_count = 15;\n  // 手机 Model, 如 \"NOH-AN01\"\n  string model = 16;\n  // 手机品牌, 如 \"HUAWEI\"\n  string brand = 17;\n  // 屏幕信息, 如 \"1288,2646,560\", 即 \"{width},{height},{pixel}\"\n  string screen = 18;\n  // CPU 型号, 留空或根据实际情况确定\n  string cpu_model = 19;\n  // 蓝牙 MAC, 留空或根据实际情况确定\n  string btmac = 20;\n  // Linux 内核 bootid\n  int64 boot = 21;\n  // 模拟器(?), 如 \"000\"\n  string emu = 22;\n  // 移动网络 MCC MNC, 如中国移动为 46007\n  string oid = 23;\n  // 当前网络类型, 如 \"WIFI\", 见 bilibili.metadata.network.NetworkType\n  string network = 24;\n  // 运行内存(Byte)\n  int64 mem = 25;\n  // 传感器信息, 为一数组直接 toString() 的结果\n  // 如 \"\"\"[\"accelerometer-icm20690,invensense\", \"akm-akm09918,akm\", \"orientation,huawei\", \"als-tcs3718,ams\", \"proximity-tcs3718,ams\", \"gyroscope-icm20690,invensense\", \"gravity,huawei\", \"linear Acceleration,huawei\", \"rotation Vector,huawei\", \"airpress-bmp280,bosch\", \"HALL sensor,huawei\", \"uncalibrated Magnetic Field,Asahi Kasei Microdevices\", \"game Rotation Vector,huawei\", \"uncalibrated Gyroscope,STMicroelectronics\", \"significant Motion,huawei\", \"step Detector,huawei\", \"step counter,huawei\", \"geomagnetic Rotation Vector,huawei\", \"phonecall sensor,huawei\", \"RPC sensor,huawei\", \"agt,huawei\", \"color sensor,huawei\", \"uncalibrated Accelerometer,huawei\", \"drop sensor,huawei\"]\"\"\"\n  string sensor = 26;\n  // CPU 频率, 如 2045000\n  int64 cpu_freq = 27;\n  // CPU 架构, 如 \"ARM\"\n  string cpu_vendor = 28;\n  // ?\n  string sim = 29;\n  // 光照传感器数值\n  int32 brightness = 30;\n  // Android Build.prop 信息, key 包括 net.hostname, ro.boot.hardware, etc.\n  // 具体 key-value 需要技术手段自行确定\n  map<string, string> props = 31;\n  // 系统信息, key 包括 product, cpu_model_name, display, cpu_abi_list, etc.\n  // 具体 key-value 需要技术手段自行确定\n  map<string, string> sys = 32;\n  // Wifi MAC, 一般无法获取, 留空\n  string wifimac = 33;\n  // Android ID\n  string adid = 34;\n  // OS 名称, 如 \"android\"\n  string os = 35;\n  // IMEI, 一般无法获取, 留空\n  string imei = 36;\n  // ?, 留空\n  string cell = 37;\n  // IMSI, 一般无法获取, 留空\n  string imsi = 38;\n  // ICCID, 一般无法获取, 留空\n  string iccid = 39;\n  // 摄像头数量, 留空\n  int32 camcnt = 40;\n  // 摄像头像素, 留空\n  string campx = 41;\n  // 手机内置存储空间(Byte)\n  int64 total_space = 42;\n  // ?, 例如 \"false\"\n  string axposed = 43;\n  // ?, 留空\n  string maps = 44;\n  // 如: \"/data/user/0/tv.danmaku.bili/files\"\n  string files = 45;\n  // 是否为虚拟化(?), 如 \"0\"\n  string virtual = 46;\n  // 虚拟进程, 如 \"[]\"\n  string virtualproc = 47;\n  // ?, 留空\n  string gadid = 48;\n  // ?, 留空\n  string glimit = 49;\n  // 设备安装的 APP 列表, 如 \"[]\"\n  string apps = 50;\n  // 客户端 GUID\n  string guid = 51;\n  // ?, 区分于用户 UID\n  string uid = 52;\n  // ?, 留空\n  int32 root = 53;\n  // 摄像头放大倍数(?), 留空\n  string camzoom = 54;\n  // 摄像头闪光灯(?), 留空\n  string camlight = 55;\n  // OAID 匿名设备标识符, 参见 T/TAF 095-2021 安卓系统补充设备标识技术规范, 默认 \"00000000-0000-0000-0000-000000000000\"\n  string oaid = 56;\n  // UDID 设备唯一标识符, 参见 T/TAF 095-2021 安卓系统补充设备标识技术规范, 可留空\n  string udid = 57;\n  // VAID 开发者匿名设备标识符, 参见 T/TAF 095-2021 安卓系统补充设备标识技术规范, 可留空\n  string vaid = 58;\n  // AAID, 应用匿名设备标识符, 参见 T/TAF 095-2021 安卓系统补充设备标识技术规范, 可留空\n  string aaid = 59;\n  // ?, 设置为 \"[]\"\n  string androidapp20 = 60;\n  // ?, 留空\n  int32 androidappcnt = 61;\n  // ?, 设置为 \"[]\"\n  string androidsysapp20 = 62;\n  // 当前剩余电量, 如 100\n  int32 battery = 63;\n  // Android 监听电量状态, 如 \"BATTERY_STATUS_DISCHARGING\"\n  string battery_state = 64;\n  // Wifi BSSID, 一般无法获取, 留空\n  string bssid = 65;\n  // ?, 如 \"NOH-AN01 4.0.0.102(DEVC00E100R7P5)\"\n  string build_id = 67;\n  // ISO 国家代码, 如 \"CN\"\n  string country_iso = 68;\n  // 可用运行内存(Byte)\n  int64 free_memory = 70;\n  // 可用内置存储空间(Byte)\n  string fstorage = 71;\n  // Linux kernel version\n  string kernel_version = 74;\n  // 语言, 如 \"zh\"\n  string languages = 75;\n  //  Wifi 网卡 MAC(?), 留空\n  string mac = 76;\n  // 当前连接 Wifi 的 SSID, 留空\n  string ssid = 79;\n  // ?, 留空\n  int32 systemvolume = 80;\n  //  Wifi 网卡 MAC 列表(?), 留空\n  string wifimaclist = 81;\n  // 运行内存(Byte)\n  int64 memory = 82;\n  // 当前剩余电量, 如 \"100\"\n  string str_battery = 83;\n  // 设备是否 Root(?), 留空\n  bool is_root = 84;\n  // 光照传感器数值字符串\n  string str_brightness = 85;\n  // 产品id, 见 2\n  string str_app_id = 86;\n  // 当前 IP(?), 留空\n  string ip = 87;\n  // 留空即可\n  string user_agent = 88;\n  // ?, 如: \"1.25\"\n  string light_intensity = 89;\n  // 设备 xyz 方向角度\n  repeated float device_angle = 90;\n  // GPS 传感器数量(或者是否有 GPS 传感器?), 如 \"1\"\n  int64 gps_sensor = 91;\n  // 速度传感器数量(或者是否有速度传感器?), 如 \"1\"\n  int64 speed_sensor = 92;\n  // 线性加速度传感器数量(或者是否有线性加速度传感器?), 如 \"1\"\n  int64 linear_speed_sensor = 93;\n  //  陀螺仪传感器数量(或者是否有陀螺仪传感器?), 如 \"1\"\n  int64 gyroscope_sensor = 94;\n  // 生物识别传感器数量(或者是否有生物识别传感器?), 如 \"1\"\n  int64 biometric = 95;\n  // 生物识别传感器类型(?), 如 \"touchid\"\n  repeated string biometrics = 96;\n  // 上次 Crash Dump 时的毫秒时间戳\n  int64 last_dump_ts = 97;\n  // 留空即可\n  string location = 98;\n  // 留空即可\n  string country = 99;\n  // 留空即可\n  string city = 100;\n  // ?, 默认为 0\n  int32 data_activity_state = 101;\n  // ?, 默认为 0\n  int32 data_connect_state = 102;\n  // ?, 默认为 0\n  int32 data_network_type = 103;\n  // ?, 默认为 0\n  int32 voice_network_type = 104;\n  // ?, 默认为 0\n  int32 voice_service_state = 105;\n  // USB 是否连接, 启用为 \"1\", 否则为 \"0\"\n  int32 usb_connected = 106;\n  // ADB 是否启用, 启用为 \"1\", 否则为 \"0\"\n  int32 adb_enabled = 107;\n  // 系统 UI 软件版本(?), 如 \"14.0.0\"\n  string ui_version = 108;\n  // 辅助服务\n  repeated string accessibility_service = 109;\n  // 传感器信息(需要和前面的 sensor 对应)\n  repeated SensorInfo sensors_info = 110;\n  // DrmId\n  string drmid = 111;\n  // 是否存在电池\n  bool battery_present = 112;\n  // 电池技术, 如 \"Li-poly\"\n  string battery_technology = 113;\n  // 电池温度(m℃)\n  int32 battery_temperature = 114;\n  // 电池电压(mV)\n  int32 battery_voltage = 115;\n  // 电池是否被拔开(?), 可以为 0\n  int32 battery_plugged = 116;\n  // 电池健康, 如 2\n  int32 battery_health = 117;\n  // 留空即可\n  repeated string cpu_abi_list = 118;\n  // 留空即可\n  string cpu_abi_libc = 119;\n  // 留空即可\n  string cpu_abi_libc64 = 120;\n  // 留空即可\n  string cpu_processor = 121;\n  // 留空即可\n  string cpu_model_name = 122;\n  // 留空即可\n  string cpu_hardware = 123;\n  // 留空即可\n  string cpu_features = 124;\n}\n\n// 传感器信息\nmessage SensorInfo {\n  // 传感器名称, 如 \"rotation Vector\"\n  string name = 1;\n  // 制造商\n  string vendor = 2;\n  //\n  int32 version = 3;\n  //\n  int32 type = 4;\n  //\n  float max_range = 5;\n  //\n  float resolution = 6;\n  // 耗电量(mA)\n  float power = 7;\n  //\n  int32 min_delay = 8;\n}\n"
  },
  {
    "path": "bili-api/grpc/proto/pgc/biz/room.proto",
    "content": "syntax = \"proto3\";\n\npackage pgc.biz;\n\noption java_multiple_files = true;\n\n//\nmessage RoomProto {\n  //\n  repeated string room_id = 1;\n}"
  },
  {
    "path": "bili-api/grpc/proto/pgc/gateway/vega/v1/vega.proto",
    "content": "syntax = \"proto3\";\n\npackage pgc.gateway.vega.v1;\n\noption java_multiple_files = true;\n\nimport \"bilibili/rpc/status.proto\";\nimport \"google/protobuf/any.proto\";\nimport \"google/protobuf/empty.proto\";\n\n//\nservice Vega {\n  //\n  rpc CreateTunnel (VegaFrame) returns (VegaFrame);\n}\n\n//\nservice VegaFrameDoc {\n  //\n  rpc Auth (AuthReq) returns (AuthResp);\n  //\n  rpc Heartbeat (HeartbeatReq) returns (HeartbeatResp);\n  //\n  rpc MessageAck (MessageAckReq) returns (google.protobuf.Empty);\n  //\n  rpc Subscribe (SubscribeReq) returns (google.protobuf.Empty);\n}\n\n//\nmessage AuthReq {}\n\n//\nmessage AuthResp {}\n\n//\nmessage FrameOption {\n  //\n  int64 vega_id = 1;\n  //\n  string req_id = 2;\n  //\n  int64 sequence = 3;\n  //\n  bool is_ack = 4;\n  //\n  bilibili.rpc.Status status = 5;\n  //\n  string ack_origin = 6;\n  //\n  int64 mid = 7;\n}\n\n//\nmessage HeartbeatReq {}\n\n//\nmessage HeartbeatResp {}\n\n//\nmessage MessageAckReq {\n  //\n  string vega_id = 1;\n  //\n  string req_id = 2;\n  //\n  string origin = 3;\n  //\n  string target_path = 4;\n}\n\n//\nmessage SubscribeReq {\n  //\n  repeated TargetPath target_paths = 1;\n}\n\n//\nmessage TargetPath {\n  //\n  string key = 1;\n  //\n  google.protobuf.Any subs = 2;\n}\n\n//\nmessage VegaFrame {\n  //\n  FrameOption options = 1;\n  //\n  string route_path = 2;\n  //\n  google.protobuf.Any body = 3;\n  //\n  google.protobuf.Any sub_biz = 4;\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/com/tfowl/ktor/client/plugins/JsoupPlugin.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.tfowl.ktor.client.plugins\n\nimport io.ktor.client.*\nimport io.ktor.client.plugins.*\nimport io.ktor.client.statement.*\nimport io.ktor.http.*\nimport io.ktor.util.*\nimport io.ktor.utils.io.*\nimport org.jsoup.Jsoup\nimport org.jsoup.nodes.Document\nimport org.jsoup.parser.Parser\n\n/**\n * [HttpClient] plugin that parses response bodies into Jsoup [Document]\n * class using a provided [Parser]\n *\n * By default,\n *\n * [ContentType.Text.Html] is parsed using [Parser.htmlParser].\n *\n * [ContentType.Text.Xml] & [ContentType.Application.Xml] are parsed using [Parser.xmlParser].\n *\n * Note: It will only parse registered content types and for receiving\n * [Document] or superclasses.\n *\n * @property parsers Registered parsers for content types\n */\nclass JsoupPlugin internal constructor(val parsers: Map<ContentType, Parser>) {\n\n    /**\n     * [JsoupPlugin] configuration that is used during installation\n     */\n    class Config {\n\n        /**\n         * [Parsers][Parser] that will be used for each [ContentType]\n         *\n         * Defaults:\n         *  - Html: [ContentType.Text.Html]\n         *  - Xml: [ContentType.Text.Xml] and [ContentType.Application.Xml]\n         */\n        var parsers = mutableMapOf(\n            ContentType.Text.Html to Parser.htmlParser(),\n            ContentType.Text.Xml to Parser.xmlParser(),\n            ContentType.Application.Xml to Parser.xmlParser()\n        )\n    }\n\n    /**\n     * Companion object for plugin installation\n     */\n    companion object Plugin : HttpClientPlugin<Config, JsoupPlugin> {\n        override val key: AttributeKey<JsoupPlugin> = AttributeKey(\"Jsoup\")\n\n        override fun prepare(block: Config.() -> Unit): JsoupPlugin =\n            JsoupPlugin(Config().apply(block).parsers)\n\n        override fun install(plugin: JsoupPlugin, scope: HttpClient) {\n            scope.responsePipeline.intercept(HttpResponsePipeline.Transform) { (info, body) ->\n                if (body !is ByteReadChannel)\n                    return@intercept\n\n                if (!info.type.java.isAssignableFrom(Document::class.java))\n                    return@intercept\n\n                val responseContentType = context.response.contentType() ?: return@intercept\n\n                val parser = plugin.parsers.firstNotNullOfOrNull { (type, parser) ->\n                    parser.takeIf { responseContentType.match(type) }\n                } ?: return@intercept\n\n                val bodyContent = body.readRemaining().readText()\n                val baseUri = context.request.url.toString()\n\n                /* Jsoup Parsers internally contain a stateful TreeBuilder,\n                   We need to create a deep copy to avoid issues with\n                   concurrency */\n                val document = Jsoup.parse(bodyContent, baseUri, parser.newInstance())\n                proceedWith(HttpResponseContainer(info, document))\n            }\n        }\n    }\n}\n\n/**\n * Install [JsoupPlugin]\n */\n@Suppress(\"FunctionName\")\nfun HttpClientConfig<*>.Jsoup(block: JsoupPlugin.Config.() -> Unit = {}) {\n    install(JsoupPlugin, block)\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/BiliApiConstants.kt",
    "content": "package dev.aaa1115910.biliapi\n\nobject BiliApiConstants {\n    const val USER_AGENT_WEB = \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36\"\n    const val USER_AGENT_APP = \"Bilibili Freedoooooom/MarkII\"\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/ApiType.kt",
    "content": "package dev.aaa1115910.biliapi.entity\n\nenum class ApiType {\n    Web, App\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/CarouselData.kt",
    "content": "package dev.aaa1115910.biliapi.entity\n\nimport dev.aaa1115910.biliapi.util.UrlUtil\nimport dev.aaa1115910.biliapi.util.toBv\nimport io.ktor.http.Url\n\ndata class CarouselData(\n    val items: List<CarouselItem>\n) {\n    companion object {\n        fun fromPgcWebInitialStateData(data: dev.aaa1115910.biliapi.http.entity.pgc.PgcWebInitialStateData): CarouselData {\n            val result = mutableListOf<CarouselItem>()\n            var needParseIdFromUrl = false\n            // 电影、电视剧、综艺板块里的轮播图数据里没有直接包含 episodeId 和 seasonId\n            if (listOf(1668, 1675, 1682).contains(data.modules.banner.moduleId))\n                needParseIdFromUrl = true\n            data.modules.banner.items.filter {\n                it.episodeId != null\n                        || (needParseIdFromUrl && it.link.contains(\"bangumi/play/ep\"))\n                        || (needParseIdFromUrl && it.link.contains(\"bangumi/play/ss\"))\n            }.forEach {\n                var cover = it.bigCover ?: it.cover\n                if (cover.startsWith(\"//\")) cover = \"https:$cover\"\n                var epidFromUrl: Int? = null\n                var ssidFromUrl: Int? = null\n\n                if (needParseIdFromUrl) {\n                    val idStr = Url(it.link).rawSegments.last()\n                    epidFromUrl =\n                        idStr.substring(2).takeIf { idStr.startsWith(\"ep\") }?.toIntOrNull()\n                    ssidFromUrl =\n                        idStr.substring(2).takeIf { idStr.startsWith(\"ss\") }?.toIntOrNull()\n                }\n\n                result.add(\n                    CarouselItem(\n                        cover = cover,\n                        title = it.title,\n                        seasonId = it.seasonId ?: ssidFromUrl ?: -1,\n                        episodeId = it.episodeId ?: epidFromUrl ?: -1\n                    )\n                )\n            }\n            return CarouselData(result)\n        }\n\n        fun fromUgcRegionDynamicBanner(data: dev.aaa1115910.biliapi.http.entity.region.RegionDynamic.Banner): CarouselData {\n            val result = mutableListOf<CarouselItem>()\n            data.top.forEach { top ->\n                if (!UrlUtil.isVideoUrl(top.uri)) return@forEach\n                val avid = UrlUtil.parseAidFromUrl(top.uri)\n                val bvid = avid.toBv()\n                result.add(\n                    CarouselItem(\n                        cover = top.image,\n                        title = top.title,\n                        avid = avid,\n                        bvid = bvid\n                    )\n                )\n            }\n            return CarouselData(result)\n        }\n\n        fun fromUgcRegionLocs(data: dev.aaa1115910.biliapi.http.entity.region.RegionLocs): CarouselData {\n            val result = mutableListOf<CarouselItem>()\n            data.data.forEach { (_, value) ->\n                value.filter { it.url.contains(\"/video/\") }.forEach { item ->\n                    result.add(\n                        CarouselItem(\n                            cover = item.pic,\n                            title = item.title,\n                            bvid = Url(item.url).rawSegments.last()\n                        )\n                    )\n                }\n            }\n            return CarouselData(result)\n        }\n\n        fun fromRegionBanner(data: dev.aaa1115910.biliapi.http.entity.region.RegionBanner): CarouselData {\n            val result = mutableListOf<CarouselItem>()\n            data.regionBannerList.forEach { item ->\n                if (!UrlUtil.isVideoUrl(item.url)) return@forEach\n                val avid = UrlUtil.parseAidFromUrl(item.url)\n                val bvid = avid.toBv()\n                result.add(\n                    CarouselItem(\n                        cover = item.image,\n                        title = item.title,\n                        avid = avid,\n                        bvid = bvid\n                    )\n                )\n            }\n            return CarouselData(result)\n        }\n    }\n\n    data class CarouselItem(\n        val cover: String,\n        val title: String,\n        val seasonId: Int? = null,\n        val episodeId: Int? = null,\n        val avid: Long? = null,\n        val bvid: String? = null\n    )\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/CodeType.kt",
    "content": "package dev.aaa1115910.biliapi.entity\n\nimport bilibili.pgc.gateway.player.v2.CodeType as PgcPlayUrlCodeType\nimport bilibili.playershared.CodeType as PlayerSharedCodeType\n\nenum class CodeType(val str: String, val codecId: Int) {\n    NoCode(\"none\", 0),\n    Code264(\"avc1\", 7),\n    Code265(\"hev1\", 12),\n    CodeAv1(\"av01\", 13),\n    Unrecognized(\"unknown\", 0);\n\n    companion object {\n        fun fromCodecId(code: Int?) = runCatching {\n            entries.find { it.codecId == code }!!\n        }.getOrDefault(NoCode)\n    }\n\n    fun toPlayerSharedCodeType() = when (this) {\n        NoCode -> PlayerSharedCodeType.NOCODE\n        Code264 -> PlayerSharedCodeType.CODE264\n        Code265 -> PlayerSharedCodeType.CODE265\n        CodeAv1 -> PlayerSharedCodeType.CODEAV1\n        Unrecognized -> PlayerSharedCodeType.UNRECOGNIZED\n    }\n\n    fun toPgcPlayUrlCodeType() = when (this) {\n        NoCode, CodeAv1 -> PgcPlayUrlCodeType.NOCODE\n        Code264 -> PgcPlayUrlCodeType.CODE264\n        Code265 -> PgcPlayUrlCodeType.CODE265\n        Unrecognized -> PgcPlayUrlCodeType.UNRECOGNIZED\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/Favorite.kt",
    "content": "package dev.aaa1115910.biliapi.entity\n\nimport dev.aaa1115910.biliapi.http.entity.user.favorite.CntInfo\nimport dev.aaa1115910.biliapi.http.entity.user.favorite.FavoriteItemId\nimport kotlinx.serialization.Serializable\n\ndata class FavoriteFolderItemId(\n    val id: Long,\n    val type: FavoriteItemType,\n    val bvid: String\n) {\n    companion object {\n        fun fromFavoriteItemId(favoriteItemId: FavoriteItemId): FavoriteFolderItemId {\n            return FavoriteFolderItemId(\n                id = favoriteItemId.id,\n                type = FavoriteItemType.fromValue(favoriteItemId.type),\n                bvid = favoriteItemId.bvid\n            )\n        }\n    }\n}\n\nenum class FavoriteItemType(val value: Int) {\n    All(0),\n    Video(2),\n    Audio(12),\n    VideoCollection(21);\n\n    companion object {\n        fun fromValue(typeId: Int) = entries.first { it.value == typeId }\n    }\n}\n\n\n/**\n * 收藏夹元数据\n *\n * @param id 收藏夹mlid（完整id） 收藏夹原始id+创建者mid尾号2位\n * @param fid 收藏夹原始id\n * @param mid 创建者mid\n * @param title 收藏夹标题\n * @param cover 收藏夹封面图片url\n * @param mediaCount 收藏夹内容数量\n */\ndata class FavoriteFolderMetadata(\n    val id: Long,\n    val fid: Long,\n    val mid: Long,\n    val title: String,\n    val cover: String?,\n    var videoInThisFav: Boolean,\n    val mediaCount: Int\n) {\n    companion object {\n        fun fromHttpFavoriteFolderInfo(httpFavoriteFolderInfo: dev.aaa1115910.biliapi.http.entity.user.favorite.FavoriteFolderInfo): FavoriteFolderMetadata {\n            return FavoriteFolderMetadata(\n                id = httpFavoriteFolderInfo.id,\n                fid = httpFavoriteFolderInfo.fid,\n                mid = httpFavoriteFolderInfo.mid,\n                title = httpFavoriteFolderInfo.title,\n                cover = httpFavoriteFolderInfo.cover,\n                videoInThisFav = httpFavoriteFolderInfo.favState == 1,\n                mediaCount = httpFavoriteFolderInfo.mediaCount\n            )\n        }\n\n        fun fromHttpUserFavoriteFolder(httpUserFavoriteFoldersData: dev.aaa1115910.biliapi.http.entity.user.favorite.UserFavoriteFoldersData.UserFavoriteFolder): FavoriteFolderMetadata {\n            return FavoriteFolderMetadata(\n                id = httpUserFavoriteFoldersData.id,\n                fid = httpUserFavoriteFoldersData.fid,\n                mid = httpUserFavoriteFoldersData.mid,\n                title = httpUserFavoriteFoldersData.title,\n                cover = null,\n                videoInThisFav = httpUserFavoriteFoldersData.favState == 1,\n                mediaCount = httpUserFavoriteFoldersData.mediaCount\n            )\n        }\n    }\n}\n\ndata class FavoriteFolderData(\n    val info: FavoriteFolderMetadata,\n    val medias: List<FavoriteItem>,\n    val hasMore: Boolean\n) {\n    companion object {\n        fun fromHttpFavoriteFolderInfoListData(httpFavoriteFolderInfoListData: dev.aaa1115910.biliapi.http.entity.user.favorite.FavoriteFolderInfoListData): FavoriteFolderData {\n            return FavoriteFolderData(\n                info = FavoriteFolderMetadata.fromHttpFavoriteFolderInfo(\n                    httpFavoriteFolderInfoListData.info\n                ),\n                medias = httpFavoriteFolderInfoListData.medias.map {\n                    FavoriteItem.fromHttpFavoriteItem(\n                        it\n                    )\n                },\n                hasMore = httpFavoriteFolderInfoListData.hasMore\n            )\n        }\n    }\n}\n\ndata class FavoriteItem(\n    val id: Long,\n    val type: FavoriteItemType,\n    val title: String,\n    val cover: String,\n    val intro: String,\n    val page: Int,\n    val duration: Int,\n    val upper: Upper,\n    val link: String,\n    val favTime: Long,\n    val bvid: String,\n    val cntInfo: CntInfo\n) {\n    companion object {\n        fun fromHttpFavoriteItem(httpFavoriteItem: dev.aaa1115910.biliapi.http.entity.user.favorite.FavoriteItem): FavoriteItem {\n            return FavoriteItem(\n                id = httpFavoriteItem.id,\n                type = FavoriteItemType.fromValue(httpFavoriteItem.type),\n                title = httpFavoriteItem.title,\n                cover = httpFavoriteItem.cover,\n                intro = httpFavoriteItem.intro,\n                page = httpFavoriteItem.page,\n                duration = httpFavoriteItem.duration,\n                upper = Upper.fromHttpUpper(httpFavoriteItem.upper),\n                link = httpFavoriteItem.link,\n                favTime = httpFavoriteItem.favTime,\n                bvid = httpFavoriteItem.bvid,\n                cntInfo = httpFavoriteItem.cntInfo\n            )\n        }\n    }\n}\n\n@Serializable\ndata class Upper(\n    val mid: Long,\n    val name: String,\n    val face: String\n) {\n    companion object {\n        fun fromHttpUpper(httpUpper: dev.aaa1115910.biliapi.http.entity.user.favorite.Upper): Upper {\n            return Upper(\n                mid = httpUpper.mid,\n                name = httpUpper.name,\n                face = httpUpper.face\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/Picture.kt",
    "content": "package dev.aaa1115910.biliapi.entity\n\nimport java.util.UUID\n\n/**\n * 评论图片\n *\n * @param url 图片链接\n * @param width 图片宽度\n * @param height 图片高度\n * @param key 使用 [com.origeek.imageViewer.previewer.TransformImageView] [com.origeek.imageViewer.previewer.ImagePreviewer] 浏览图片缩放时需要用到的 key\n */\ndata class Picture(\n    val url: String,\n    val width: Int,\n    val height: Int,\n    val key: String\n) {\n    companion object {\n        private fun normalizeUrl(url: String): String {\n            return when {\n                url.startsWith(\"http://\") -> url.replaceFirst(\"http://\", \"https://\")\n                else -> url\n            }\n        }\n\n        fun fromPicture(picture: dev.aaa1115910.biliapi.http.entity.reply.CommentData.Reply.Content.Picture): Picture {\n            return Picture(\n                url = normalizeUrl(picture.imgSrc),\n                width = picture.imgWidth,\n                height = picture.imgHeight,\n                key = UUID.randomUUID().toString()\n            )\n        }\n\n        fun fromPicture(picture: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Dynamic.Major.Opus.Pic): Picture {\n            return Picture(\n                url = normalizeUrl(picture.url),\n                width = picture.width,\n                height = picture.height,\n                key = UUID.randomUUID().toString()\n            )\n        }\n\n        fun fromPicture(picture: bilibili.main.community.reply.v1.Picture): Picture {\n            return Picture(\n                url = normalizeUrl(picture.imgSrc),\n                width = picture.imgWidth.toInt(),\n                height = picture.imgHeight.toInt(),\n                key = UUID.randomUUID().toString()\n            )\n        }\n\n        fun fromPicture(picture: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Dynamic.Major.Draw.Pic): Picture {\n            return Picture(\n                url = normalizeUrl(picture.src),\n                width = picture.width,\n                height = picture.height,\n                key = UUID.randomUUID().toString()\n            )\n        }\n\n        fun fromPicture(picture: bilibili.app.dynamic.v2.MdlDynDrawItem): Picture {\n            return Picture(\n                url = normalizeUrl(picture.src),\n                width = picture.width.toInt(),\n                height = picture.height.toInt(),\n                key = UUID.randomUUID().toString()\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/PlayData.kt",
    "content": "package dev.aaa1115910.biliapi.entity\n\nimport bilibili.app.playerunite.v1.PlayViewUniteReply\nimport bilibili.pgc.gateway.player.v2.dashVideoOrNull\nimport bilibili.pgc.gateway.player.v2.dolbyOrNull\nimport bilibili.playershared.dashVideoOrNull\nimport bilibili.playershared.dolbyOrNull\nimport bilibili.playershared.lossLessItemOrNull\nimport bilibili.playershared.segmentVideoOrNull\nimport dev.aaa1115910.biliapi.http.entity.video.ClipInfo\n\ndata class PlayData(\n    val dashVideos: List<DashVideo>,\n    val dashAudios: List<DashAudio>,\n    val dolby: DashAudio? = null,\n    val flac: DashAudio? = null,\n    val codec: Map<Int, List<String>> = emptyMap(),\n    val needPay: Boolean = false,\n    val clipInfoList: List<ClipInfo> = emptyList(),\n) {\n    companion object {\n        fun fromPlayViewUniteReply(playViewUniteReply: PlayViewUniteReply): PlayData {\n            val vodInfo = playViewUniteReply.vodInfo\n\n            // 过滤出有 dashVideo 的流\n            val dashVideoStreams = vodInfo.streamListList.filter { it.dashVideoOrNull != null }\n            // 过滤出有 segmentVideo 的流（试看流）\n            val segmentVideoStreams = vodInfo.streamListList.filter { it.segmentVideoOrNull != null }\n\n            val audioList = vodInfo.dashAudioList\n            val dolbyItem = vodInfo.dolbyOrNull?.audioList?.firstOrNull()\n            val lossLessItem =\n                vodInfo.lossLessItemOrNull?.audio.takeIf { it?.id != 0 }\n\n            // 处理 dashVideo\n            val dashVideos = dashVideoStreams.map {\n                DashVideo(\n                    quality = it.streamInfo.quality,\n                    baseUrl = it.dashVideo.baseUrl,\n                    bandwidth = it.dashVideo.bandwidth,\n                    codecId = it.dashVideo.codecid,\n                    width = it.dashVideo.width,\n                    height = it.dashVideo.height,\n                    frameRate = it.dashVideo.frameRate,\n                    backUrl = it.dashVideo.backupUrlList,\n                    codecs = CodeType.fromCodecId(it.dashVideo.codecid).str\n                )\n            }.toMutableList()\n\n            val isPreview = dashVideos.isEmpty() && segmentVideoStreams.isNotEmpty()\n\n            // 当 dashVideo 不存在时，使用 segmentVideo（试看流）的 durl 填充\n            if (isPreview) {\n                segmentVideoStreams.forEach { stream ->\n                    val firstSegment = stream.segmentVideo.segmentList.firstOrNull()\n                    if (firstSegment != null) {\n                        dashVideos.add(\n                            DashVideo(\n                                quality = stream.streamInfo.quality,\n                                baseUrl = firstSegment.url,\n                                bandwidth = 0,\n                                codecId = stream.streamInfo.quality,\n                                width = 0,\n                                height = 0,\n                                frameRate = \"\",\n                                backUrl = firstSegment.backupUrlList,\n                                codecs = CodeType.fromCodecId(stream.streamInfo.quality).str\n                            )\n                        )\n                    }\n                }\n            }\n            val dashAudios = audioList.map {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrlList\n                )\n            }\n            val dolby = dolbyItem?.let {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrlList\n                )\n            }\n            val flac = lossLessItem?.let {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrlList\n                )\n            }\n\n            // 生成 codec 映射（优先使用 dashVideo，如果没有则使用 segmentVideo）\n            val codecs = if (dashVideoStreams.isNotEmpty()) {\n                dashVideoStreams.associate {\n                    it.streamInfo.quality to listOf(CodeType.fromCodecId(it.dashVideo.codecid).str)\n                }\n            } else {\n                segmentVideoStreams.associate {\n                    it.streamInfo.quality to listOf(CodeType.fromCodecId(it.streamInfo.quality).str)\n                }\n            }\n\n            return PlayData(\n                dashVideos = dashVideos,\n                dashAudios = dashAudios,\n                dolby = dolby,\n                flac = flac,\n                codec = codecs,\n                needPay = isPreview\n            )\n        }\n\n        fun fromPgcPlayViewReply(pgcPlayViewReply: bilibili.pgc.gateway.player.v2.PlayViewReply): PlayData {\n            val streamList =\n                pgcPlayViewReply.videoInfo.streamListList.filter { it.dashVideoOrNull != null }\n            val audioList = pgcPlayViewReply.videoInfo.dashAudioList\n            val dolbyItem = pgcPlayViewReply.videoInfo.dolbyOrNull?.audio\n            val codecs = pgcPlayViewReply.videoInfo.streamListList.associate {\n                it.info.quality to listOf(CodeType.fromCodecId(it.dashVideo.codecid).str)\n            }\n            val needPay = pgcPlayViewReply.business.isPreview\n\n            val dashVideos = streamList.map {\n                DashVideo(\n                    quality = it.info.quality,\n                    baseUrl = it.dashVideo.baseUrl,\n                    bandwidth = it.dashVideo.bandwidth,\n                    codecId = it.dashVideo.codecid,\n                    width = it.dashVideo.width,\n                    height = it.dashVideo.height,\n                    frameRate = it.dashVideo.frameRate,\n                    backUrl = it.dashVideo.backupUrlList,\n                    codecs = CodeType.fromCodecId(it.dashVideo.codecid).str\n                )\n            }\n            val dashAudios = audioList.map {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrlList\n                )\n            }\n            val dolby = dolbyItem?.let {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.codecid,\n                    backUrl = it.backupUrlList\n                )\n            }\n\n            return PlayData(\n                dashVideos = dashVideos,\n                dashAudios = dashAudios,\n                dolby = dolby,\n                flac = null,\n                codec = codecs,\n                needPay = needPay\n            )\n        }\n\n        fun fromPlayUrlV2Data(playUrlV2Data: dev.aaa1115910.biliapi.http.entity.video.PlayUrlV2Data): PlayData {\n            return fromPlayUrlData(playUrlV2Data.videoInfo)\n        }\n\n        fun fromPlayUrlData(playUrlData: dev.aaa1115910.biliapi.http.entity.video.PlayUrlData): PlayData {\n            val hasDash = playUrlData.dash != null\n            val isPreview = !hasDash && playUrlData.durl.isNotEmpty()\n\n            val audios = playUrlData.dash?.audio\n            val dolbyItem = playUrlData.dash?.dolby?.audio?.firstOrNull()\n            val flacItem = playUrlData.dash?.flac?.audio\n            val codec = if (hasDash) {\n                playUrlData.supportFormats\n                    .mapNotNull { it.codecs?.let { c -> it.quality to c } }\n                    .toMap()\n            } else {\n                mapOf(playUrlData.quality to listOf(CodeType.fromCodecId(playUrlData.videoCodecId).str))\n            }\n\n            val dashVideos = if (hasDash) {\n                playUrlData.dash!!.video.map {\n                    DashVideo(\n                        quality = it.id,\n                        baseUrl = it.baseUrl,\n                        bandwidth = it.bandwidth,\n                        codecId = it.id,\n                        width = it.width,\n                        height = it.height,\n                        frameRate = it.frameRate,\n                        backUrl = it.backupUrl,\n                        codecs = it.codecs\n                    )\n                }\n            } else {\n                // 充电视频未付费状态下没有 dash，只有试看流 durl，转成 DASH 结构\n                playUrlData.durl.map {\n                    DashVideo(\n                        quality = playUrlData.quality,\n                        baseUrl = it.url,\n                        backUrl = it.backupUrl,\n                        codecId = playUrlData.videoCodecId,\n                        bandwidth = 0, width = 0, height = 0, frameRate = \"\", codecs = \"\"\n                    )\n                }\n            }\n            val dashAudios = audios?.map {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrl\n                )\n            } ?: emptyList()\n            val dolby = dolbyItem?.let {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrl\n                )\n            }\n            val flac = flacItem?.let {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrl\n                )\n            }\n\n            return PlayData(\n                dashVideos = dashVideos,\n                dashAudios = dashAudios,\n                dolby = dolby,\n                flac = flac,\n                codec = codec,\n                needPay = isPreview,\n                clipInfoList = playUrlData.clipInfoList\n            )\n        }\n\n        fun fromPlayUrlData(playUrlData: dev.aaa1115910.biliapi.http.entity.proxy.ProxyWebPlayUrlData): PlayData {\n            val hasDash = playUrlData.dash != null\n            val isPreview = !hasDash && playUrlData.durl.isNotEmpty()\n\n            val audios = playUrlData.dash?.audio\n            val dolbyItem = playUrlData.dash?.dolby?.audio?.firstOrNull()\n            val flacItem = playUrlData.dash?.flac?.audio\n            val codec = if (hasDash) {\n                playUrlData.supportFormats\n                    .mapNotNull { it.codecs?.let { c -> it.quality to c } }\n                    .toMap()\n            } else {\n                mapOf(playUrlData.quality to listOf(CodeType.fromCodecId(playUrlData.videoCodecId).str))\n            }\n\n            val dashVideos = if (hasDash) {\n                playUrlData.dash!!.video.map {\n                    DashVideo(\n                        quality = it.id,\n                        baseUrl = it.baseUrl,\n                        bandwidth = it.bandwidth,\n                        codecId = it.id,\n                        width = it.width,\n                        height = it.height,\n                        frameRate = it.frameRate,\n                        backUrl = it.backupUrl,\n                        codecs = it.codecs\n                    )\n                }\n            } else {\n                playUrlData.durl.map {\n                    DashVideo(\n                        quality = playUrlData.quality,\n                        baseUrl = it.url,\n                        backUrl = it.backupUrl,\n                        codecId = playUrlData.videoCodecId,\n                        bandwidth = 0, width = 0, height = 0, frameRate = \"\", codecs = \"\"\n                    )\n                }\n            }\n            val dashAudios = audios?.map {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrl\n                )\n            } ?: emptyList()\n            val dolby = dolbyItem?.let {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrl\n                )\n            }\n            val flac = flacItem?.let {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrl\n                )\n            }\n\n            return PlayData(\n                dashVideos = dashVideos,\n                dashAudios = dashAudios,\n                dolby = dolby,\n                flac = flac,\n                codec = codec,\n                needPay = isPreview,\n                clipInfoList = playUrlData.clipInfoList\n            )\n        }\n\n        fun fromPlayUrlData(playUrlData: dev.aaa1115910.biliapi.http.entity.proxy.ProxyAppPlayUrlData): PlayData {\n            val hasDash = playUrlData.dash != null\n            val isPreview = !hasDash && playUrlData.durl.isNotEmpty()\n\n            val audios = playUrlData.dash?.audio\n            val dolbyItem = playUrlData.dash?.dolby?.audio?.firstOrNull()\n            val flacItem = playUrlData.dash?.flac?.audio\n            val codec = if (hasDash) {\n                playUrlData.supportFormats\n                    .mapNotNull { it.codecs?.let { c -> it.quality to c } }\n                    .toMap()\n            } else {\n                mapOf(playUrlData.quality to listOf(CodeType.fromCodecId(playUrlData.videoCodecId).str))\n            }\n\n            val dashVideos = if (hasDash) {\n                playUrlData.dash!!.video.map {\n                    DashVideo(\n                        quality = it.id,\n                        baseUrl = it.baseUrl,\n                        bandwidth = it.bandwidth,\n                        codecId = it.id,\n                        width = it.width,\n                        height = it.height,\n                        frameRate = it.frameRate,\n                        backUrl = it.backupUrl,\n                        codecs = it.codecs\n                    )\n                }\n            } else {\n                playUrlData.durl.map {\n                    DashVideo(\n                        quality = playUrlData.quality,\n                        baseUrl = it.url,\n                        backUrl = it.backupUrl,\n                        codecId = playUrlData.videoCodecId,\n                        bandwidth = 0, width = 0, height = 0, frameRate = \"\", codecs = \"\"\n                    )\n                }\n            }\n            val dashAudios = audios?.map {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrl\n                )\n            } ?: emptyList()\n            val dolby = dolbyItem?.let {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrl\n                )\n            }\n            val flac = flacItem?.let {\n                DashAudio(\n                    baseUrl = it.baseUrl,\n                    bandwidth = it.bandwidth,\n                    codecId = it.id,\n                    backUrl = it.backupUrl\n                )\n            }\n\n            return PlayData(\n                dashVideos = dashVideos,\n                dashAudios = dashAudios,\n                dolby = dolby,\n                flac = flac,\n                codec = codec,\n                needPay = isPreview,\n                clipInfoList = playUrlData.clipInfoList\n            )\n        }\n    }\n\n    operator fun plus(other: PlayData): PlayData {\n        return PlayData(\n            dashVideos = (dashVideos + other.dashVideos)\n                .distinctBy { \"${it.codecId}_${it.quality}\" }\n                .sortedByDescending { it.quality },\n            dashAudios = (dashAudios + other.dashAudios)\n                .distinctBy { it.codecId }\n                .sortedByDescending { it.codecId },\n            dolby = dolby ?: other.dolby,\n            flac = flac ?: other.flac,\n            codec = codec.map {\n                it.key to (it.value + other.codec[it.key].orEmpty())\n                    .distinct()\n                    .filter { it != \"none\" }\n            }.toMap(),\n            needPay = needPay || other.needPay,\n            clipInfoList = clipInfoList + other.clipInfoList\n        )\n    }\n}\n\n/**\n * @param quality 视频分辨率\n * @param baseUrl 主线流\n * @param bandwidth 码率\n * @param codecId 编码ID\n * @param width 视频宽度\n * @param height 视频高度\n * @param frameRate 帧率\n * @param backUrl 备用流\n * @param codecs 编码格式 仅 Web 接口有该值\n */\ndata class DashVideo(\n    val quality: Int,\n    val baseUrl: String,\n    val bandwidth: Int,\n    val codecId: Int,\n    val width: Int,\n    val height: Int,\n    val frameRate: String,\n    val backUrl: List<String>,\n    val codecs: String? = null\n)\n\n/**\n * @param baseUrl 主线流\n * @param bandwidth 码率\n * @param codecId 编码ID\n * @param backUrl 备用流\n */\ndata class DashAudio(\n    val baseUrl: String,\n    val bandwidth: Int,\n    val codecId: Int,\n    val backUrl: List<String>\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/danmaku/DanmakuMask.kt",
    "content": "package dev.aaa1115910.biliapi.entity.danmaku\n\nimport io.ktor.util.decodeBase64String\nimport okio.ByteString.Companion.readByteString\nimport java.io.ByteArrayInputStream\nimport java.io.DataInputStream\nimport java.util.zip.GZIPInputStream\n\ndata class DanmakuMaskSegment(\n    val range: LongRange,\n    val frames: List<DanmakuMaskFrame>,\n)\n\nsealed class DanmakuMaskFrame(\n    open val range: LongRange\n)\n\ndata class DanmakuWebMaskFrame(\n    override val range: LongRange,\n    val svg: String\n) : DanmakuMaskFrame(range)\n\ndata class DanmakuMobMaskFrame(\n    override val range: LongRange,\n    val width: Int,\n    val height: Int,\n    val image: ByteArray\n) : DanmakuMaskFrame(range) {\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n\n        other as DanmakuMobMaskFrame\n\n        if (range != other.range) return false\n        if (width != other.width) return false\n        if (height != other.height) return false\n        if (!image.contentEquals(other.image)) return false\n\n        return true\n    }\n\n    override fun hashCode(): Int {\n        var result = range.hashCode()\n        result = 31 * result + width\n        result = 31 * result + height\n        result = 31 * result + image.contentHashCode()\n        return result\n    }\n}\n\ndata class DanmakuMask(\n    val type: DanmakuMaskType,\n    val segments: List<DanmakuMaskSegment>\n) {\n    companion object {\n        fun fromBinary(binary: ByteArray, type: DanmakuMaskType): DanmakuMask {\n            val times = mutableListOf<Long>()\n            val offsets = mutableListOf<Long>()\n            val danmakuMaskSegments = mutableListOf<DanmakuMaskSegment>()\n\n            val inputStream = DataInputStream(ByteArrayInputStream(binary))\n            val mask = inputStream.readByteString(4)\n            require(mask.string(Charsets.UTF_8) == \"MASK\") { \"Not a mask file\" }\n            val version = inputStream.readInt()\n            val unused = inputStream.readByteString(4)\n            val size = inputStream.readInt()\n\n            for (i in 0 until size) {\n                times.add(inputStream.readLong())\n                offsets.add(inputStream.readLong())\n            }\n\n            var lastTime = 0L\n            var segLastTime = 0L\n\n            for (i in 0 until size) {\n                val frameList = mutableListOf<DanmakuMaskFrame>()\n\n                val bytes = if (i == size - 1) {\n                    inputStream.readBytes()\n                } else {\n                    val offDiff = (offsets[i + 1] - offsets[i]).toInt()\n                    val byteArray = ByteArray(offDiff)\n                    inputStream.read(byteArray)\n                    byteArray\n                }\n\n                val stream =\n                    DataInputStream(ByteArrayInputStream(GZIPInputStream(bytes.inputStream()).readBytes()))\n                while (stream.available() != 0) {\n                    when (type) {\n                        DanmakuMaskType.WebMask -> {\n                            val svgLength = stream.readInt()\n                            val time = stream.readLong()\n                            val svg = stream.readByteString(svgLength).string(Charsets.UTF_8)\n\n                            val svgParts = svg.split(\",\")\n                            if (svgParts.size < 2) {\n                                throw IllegalArgumentException(\"Invalid SVG format\")\n                            }\n                            val decodedSvg = svgParts[1]\n                                .replace(\"\\n\", \"\")\n                                .decodeBase64String()\n                            frameList.add(\n                                DanmakuWebMaskFrame(\n                                    range = lastTime..<time,\n                                    svg = decodedSvg\n                                )\n                            )\n                            lastTime = time\n                        }\n\n                        DanmakuMaskType.MobMask -> {\n                            val width = stream.readShort().toInt()\n                            val height = stream.readShort().toInt()\n                            val time = stream.readLong()\n                            val imageSize = (width * height + 7) / 8  // 1bpp\n                            val imageBinary = ByteArray(imageSize)\n                            stream.read(imageBinary)\n                            frameList.add(\n                                DanmakuMobMaskFrame(\n                                    range = lastTime..<time,\n                                    width = width,\n                                    height = height,\n                                    image = imageBinary\n                                )\n                            )\n                            lastTime = time\n                        }\n                    }\n                }\n\n                val startTime = segLastTime\n                val endTime = if (i == size - 1) Long.MAX_VALUE else times[i + 1]\n                danmakuMaskSegments.add(\n                    DanmakuMaskSegment(range = startTime..<endTime, frames = frameList)\n                )\n                segLastTime = endTime\n            }\n\n            return DanmakuMask(type, danmakuMaskSegments)\n        }\n    }\n}\n\nenum class DanmakuMaskType {\n    WebMask, MobMask\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/home/RecommendData.kt",
    "content": "package dev.aaa1115910.biliapi.entity.home\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\n\ndata class RecommendData(\n    val items: List<UgcItem>,\n    val nextPage: RecommendPage\n)\n\ndata class RecommendPage(\n    val nextWebIdx: Int = 1,\n    val nextAppIdx: Int = 0\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/live/LiveArea.kt",
    "content": "package dev.aaa1115910.biliapi.entity.live\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class LiveAreaResponse(\n    val code: Int,\n    val msg: String,\n    val message: String,\n    val data: List<LiveAreaGroup> = emptyList()\n)\n\n@Serializable\ndata class LiveAreaGroup(\n    val id: Int,\n    val name: String,\n    val list: List<LiveAreaItem> = emptyList()\n)\n\n@Serializable\ndata class LiveAreaItem(\n    val id: String,\n    @SerialName(\"parent_id\") val parentId: String,\n    @SerialName(\"old_area_id\") val oldAreaId: String,\n    val name: String,\n    val pic: String,\n    @SerialName(\"parent_name\") val parentName: String,\n    @SerialName(\"area_type\") val areaType: Int\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/live/LiveFollowing.kt",
    "content": "package dev.aaa1115910.biliapi.entity.live\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 关注的直播间列表响应\n * API: https://api.live.bilibili.com/xlive/web-ucenter/user/following\n */\n@Serializable\ndata class LiveFollowingResponse(\n    val code: Int,\n    val message: String,\n    val data: LiveFollowingData? = null\n)\n\n@Serializable\ndata class LiveFollowingData(\n    @SerialName(\"totalPage\") val totalPage: Int = 0,\n    val count: Int = 0,\n    val list: List<LiveFollowingRoom> = emptyList()\n)\n\n@Serializable\ndata class LiveFollowingRoom(\n    @SerialName(\"roomid\") val roomId: Int = 0,\n    val uid: Long = 0,\n    val title: String = \"\",\n    val uname: String = \"\",\n    @SerialName(\"room_cover\") val roomCover: String = \"\",\n    @SerialName(\"cover_from_user\") val coverFromUser: String = \"\",\n    val face: String = \"\",\n    @SerialName(\"text_small\") val textSmall: String = \"\",\n    @SerialName(\"live_status\") val liveStatus: Int = 0,\n    @SerialName(\"parent_area_id\") val parentAreaId: Int = 0,\n    @SerialName(\"area_v2_parent_name\") val parentAreaName: String = \"\",\n    @SerialName(\"area_id\") val areaId: Int = 0,\n    @SerialName(\"area_name_v2\") val areaNameV2: String = \"\",\n    @SerialName(\"area_name\") val areaName: String = \"\",\n    @SerialName(\"watched_show\") val watchedShow: WatchedShow? = null\n) {\n    /** 是否正在直播 */\n    val isLive: Boolean get() = liveStatus == 1\n\n    /** 封面优先使用 room_cover，降级到 cover_from_user */\n    val coverUrl: String\n        get() = roomCover.ifBlank { coverFromUser }\n\n    /** 解析中文数字格式（如 \"1.2万\"）为整数 */\n    val onlineCount: Int\n        get() = parseCnCount(textSmall)\n\n    fun toLiveRoomItem(): LiveRoomItem = LiveRoomItem(\n        roomId = roomId,\n        uid = uid,\n        title = title,\n        uname = uname,\n        online = onlineCount,\n        userCover = coverUrl,\n        cover = coverUrl,\n        face = face,\n        parentId = parentAreaId,\n        parentName = parentAreaName,\n        areaId = areaId,\n        areaName = areaNameV2.ifBlank { areaName },\n        watchedShow = watchedShow,\n        liveStatus = liveStatus\n    )\n\n    companion object {\n        /**\n         * 解析中文计数格式（如 \"1.2万\"）为整数\n         */\n        private fun parseCnCount(text: String): Int {\n            if (text.isBlank()) return 0\n            val trimmed = text.trim()\n            return when {\n                trimmed.endsWith(\"万\") -> {\n                    val num = trimmed.removeSuffix(\"万\").toDoubleOrNull() ?: return 0\n                    (num * 10000).toInt()\n                }\n                trimmed.endsWith(\"亿\") -> {\n                    val num = trimmed.removeSuffix(\"亿\").toDoubleOrNull() ?: return 0\n                    (num * 100000000).toInt()\n                }\n                else -> trimmed.toIntOrNull() ?: 0\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/live/LiveRecommend.kt",
    "content": "package dev.aaa1115910.biliapi.entity.live\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 推荐直播列表响应（主端点）\n * API: https://api.live.bilibili.com/xlive/web-interface/v1/webMain/getMoreRecList\n */\n@Serializable\ndata class LiveRecommendResponse(\n    val code: Int,\n    val message: String,\n    val data: LiveRecommendData? = null\n)\n\n@Serializable\ndata class LiveRecommendData(\n    @SerialName(\"recommend_room_list\") val recommendRoomList: List<LiveRecommendRoom> = emptyList()\n)\n\n@Serializable\ndata class LiveRecommendRoom(\n    @SerialName(\"roomid\") val roomId: Int = 0,\n    val uid: Long = 0,\n    val title: String = \"\",\n    val uname: String = \"\",\n    val online: Int = 0,\n    @SerialName(\"user_cover\") val userCover: String = \"\",\n    val cover: String = \"\",\n    val face: String = \"\",\n    @SerialName(\"area_v2_parent_id\") val parentAreaId: Int = 0,\n    @SerialName(\"area_v2_parent_name\") val parentAreaName: String = \"\",\n    @SerialName(\"area_v2_id\") val areaId: Int = 0,\n    @SerialName(\"area_v2_name\") val areaName: String = \"\",\n    val keyframe: String = \"\",\n    @SerialName(\"watched_show\") val watchedShow: WatchedShow? = null\n) {\n    fun toLiveRoomItem(): LiveRoomItem = LiveRoomItem(\n        roomId = roomId,\n        uid = uid,\n        title = title,\n        uname = uname,\n        online = online,\n        userCover = userCover,\n        cover = cover,\n        face = face,\n        parentId = parentAreaId,\n        parentName = parentAreaName,\n        areaId = areaId,\n        areaName = areaName,\n        watchedShow = watchedShow\n    )\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/live/LiveRoom.kt",
    "content": "package dev.aaa1115910.biliapi.entity.live\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class LiveRoomListResponse(\n    val code: Int,\n    val message: String,\n    val data: LiveRoomListData = LiveRoomListData()\n)\n\n@Serializable\ndata class LiveRoomListData(\n    val list: List<LiveRoomItem> = emptyList()\n)\n\n@Serializable\ndata class LiveRoomItem(\n    @SerialName(\"roomid\") val roomId: Int = 0,\n    val uid: Long = 0,\n    val title: String = \"\",\n    val uname: String = \"\",\n    val online: Int = 0,\n    @SerialName(\"user_cover\") val userCover: String = \"\",\n    @SerialName(\"system_cover\") val systemCover: String = \"\",\n    val cover: String = \"\",\n    val face: String = \"\",\n    @SerialName(\"parent_id\") val parentId: Int = 0,\n    @SerialName(\"parent_name\") val parentName: String = \"\",\n    @SerialName(\"area_id\") val areaId: Int = 0,\n    @SerialName(\"area_name\") val areaName: String = \"\",\n    @SerialName(\"watched_show\") val watchedShow: WatchedShow? = null,\n    /** 直播状态：1=直播中, 0=未开播。非序列化字段，由 toLiveRoomItem 注入 */\n    @kotlinx.serialization.Transient val liveStatus: Int = 1\n)\n\n@Serializable\ndata class WatchedShow(\n    val switch: Boolean = false,\n    val num: Int = 0,\n    @SerialName(\"text_small\") val textSmall: String = \"\",\n    @SerialName(\"text_large\") val textLarge: String = \"\",\n    val icon: String = \"\",\n    @SerialName(\"icon_location\") val iconLocation: Int = 0,\n    @SerialName(\"icon_web\") val iconWeb: String = \"\"\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/live/LiveRoomPlayInfo.kt",
    "content": "package dev.aaa1115910.biliapi.entity.live\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class LiveRoomPlayInfoResponse(\n    val code: Int,\n    val message: String,\n    val data: LiveRoomPlayInfoData? = null\n)\n\n@Serializable\ndata class LiveRoomPlayInfoData(\n    @SerialName(\"room_id\") val roomId: Int,\n    @SerialName(\"short_id\") val shortId: Int,\n    val uid: Long,\n    @SerialName(\"live_status\") val liveStatus: Int, // 0=未开播, 1=直播中\n    @SerialName(\"is_portrait\") val isPortrait: Boolean,\n    @SerialName(\"playurl_info\") val playUrlInfo: LivePlayUrlInfo? = null\n)\n\n@Serializable\ndata class LivePlayUrlInfo(\n    val playurl: LivePlayUrl? = null\n)\n\n@Serializable\ndata class LiveQnDesc(\n    val qn: Int,\n    val desc: String\n)\n\n@Serializable\ndata class LivePlayUrl(\n    val stream: List<LiveStream> = emptyList(),\n    @SerialName(\"g_qn_desc\") val gQnDesc: List<LiveQnDesc> = emptyList()\n)\n\n@Serializable\ndata class LiveStream(\n    @SerialName(\"protocol_name\") val protocolName: String,\n    val format: List<LiveFormat> = emptyList()\n)\n\n@Serializable\ndata class LiveFormat(\n    @SerialName(\"format_name\") val formatName: String,\n    val codec: List<LiveCodec> = emptyList()\n)\n\n@Serializable\ndata class LiveCodec(\n    @SerialName(\"codec_name\") val codecName: String,\n    @SerialName(\"current_qn\") val currentQn: Int,\n    @SerialName(\"accept_qn\") val acceptQn: List<Int> = emptyList(),\n    @SerialName(\"base_url\") val baseUrl: String,\n    @SerialName(\"url_info\") val urlInfo: List<LiveUrlInfo> = emptyList()\n)\n\n@Serializable\ndata class LiveUrlInfo(\n    val host: String,\n    val extra: String\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/login/Captcha.kt",
    "content": "package dev.aaa1115910.biliapi.entity.login\n\n/**\n * 人机数据\n *\n * @param token 登录 API token\n * @param gt 极验id\t一般为固定值\n * @param challenge 极验KEY\t由B站后端产生用于人机验证\n */\ndata class Captcha(\n    val token: String,\n    val challenge: String,\n    val gt: String\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/login/QR.kt",
    "content": "package dev.aaa1115910.biliapi.entity.login\n\nimport java.util.Date\n\n/**\n * 获取扫码登录的二维码\n *\n * @param url 二维码内容\n * @param key 用于查询扫码登录结果\n */\ndata class QrLoginData(\n    val url: String,\n    val key: String\n)\n\n/**\n * 扫码登录结果\n *\n * @param state 登录结果状态\n * @param cookies 登录成功的 cookies\n */\ndata class QrLoginResult(\n    val state: QrLoginState,\n    val accessToken: String? = null,\n    val refreshToken: String? = null,\n    val cookies: WebCookies? = null\n)\n\nenum class QrLoginState {\n    Ready,\n    RequestingQRCode,\n    WaitingForScan,\n    WaitingForConfirm,\n    Expired,\n    Success,\n    Error,\n    Unknown\n}\n\ndata class WebCookies(\n    val dedeUserId: Long,\n    val dedeUserIdCkMd5: String,\n    val sid: String,\n    val biliJct: String,\n    val sessData: String,\n    val expiredDate: Date\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/login/Sms.kt",
    "content": "package dev.aaa1115910.biliapi.entity.login\n\nimport dev.aaa1115910.biliapi.http.entity.login.sms.SmsLoginResponse\nimport java.util.Date\n\ndata class SmsLoginResult(\n    val status: Int,\n    val message: String,\n    val accessToken: String,\n    val refreshToken: String,\n    val sessData: String,\n    val biliJct: String,\n    val dedeUserId: Long,\n    val dedeUserIdCkMd5: String,\n    val sid: String,\n    val expiredDate: Date\n) {\n    companion object {\n        fun fromSmsLoginResponse(smsLoginResponse: SmsLoginResponse) = SmsLoginResult(\n            status = smsLoginResponse.status,\n            message = smsLoginResponse.message,\n            accessToken = smsLoginResponse.tokenInfo!!.accessToken,\n            refreshToken = smsLoginResponse.tokenInfo.refreshToken,\n            sessData = smsLoginResponse.cookieInfo!!.cookies.find { it.name == \"SESSDATA\" }?.value\n                ?: \"\",\n            biliJct = smsLoginResponse.cookieInfo.cookies.find { it.name == \"bili_jct\" }?.value\n                ?: \"\",\n            dedeUserId = smsLoginResponse.cookieInfo.cookies.find { it.name == \"DedeUserID\" }?.value?.toLongOrNull()\n                ?: 0,\n            dedeUserIdCkMd5 = smsLoginResponse.cookieInfo.cookies.find { it.name == \"DedeUserID__ckMd5\" }?.value\n                ?: \"\",\n            sid = smsLoginResponse.cookieInfo.cookies.find { it.name == \"sid\" }?.value ?: \"\",\n            expiredDate = Date(System.currentTimeMillis() + smsLoginResponse.tokenInfo.expiresIn * 1000L)\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcFeedData.kt",
    "content": "package dev.aaa1115910.biliapi.entity.pgc\n\ndata class PgcFeedData(\n    var hasNext: Boolean,\n    var cursor: Int,\n    var items: List<PgcItem> = emptyList(),\n    var ranks: List<FeedRank> = emptyList()\n) {\n    companion object {\n        fun fromPgcFeedData(data: dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedData): PgcFeedData {\n            return PgcFeedData(\n                hasNext = data.hasNext,\n                cursor = data.coursor,\n                items = data.items.map { PgcItem.fromFeedSubItem(it) },\n                ranks = emptyList()\n            )\n        }\n\n        fun fromPgcFeedData(data: dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedV3Data): PgcFeedData {\n            val itemsList = data.items.find { it.subItems.first().cardStyle == \"v_card\" }\n            val ranksList = data.items.find { it.subItems.first().cardStyle == \"rank\" }\n            return PgcFeedData(\n                hasNext = data.hasNext,\n                cursor = data.coursor,\n                items = itemsList?.subItems?.map { PgcItem.fromFeedSubItem(it) } ?: emptyList(),\n                ranks = ranksList?.subItems?.map { FeedRank.fromFeedSubItem(it) } ?: emptyList()\n            )\n        }\n    }\n\n    data class FeedRank(\n        var cover: String,\n        var title: String,\n        var subTitle: String,\n        var items: List<PgcItem>\n    ) {\n        companion object {\n            fun fromFeedSubItem(feedSubItem: dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedV3Data.FeedItem.FeedSubItem): FeedRank {\n                return FeedRank(\n                    cover = feedSubItem.cover,\n                    title = feedSubItem.title,\n                    subTitle = feedSubItem.subTitle,\n                    items = feedSubItem.subItems?.map { PgcItem.fromFeedSubItem(it) }\n                        ?: emptyList()\n                )\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcItem.kt",
    "content": "package dev.aaa1115910.biliapi.entity.pgc\n\nimport dev.aaa1115910.biliapi.http.SeasonIndexType\n\ndata class PgcItem(\n    var cover: String,\n    var title: String,\n    var subTitle: String,\n    var seasonId: Int,\n    var episodeId: Int,\n    var seasonType: SeasonIndexType,\n    var rating: String\n) {\n    companion object {\n        fun fromFeedSubItem(feedSubItem: dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedData.FeedSubItem): PgcItem {\n            return PgcItem(\n                cover = feedSubItem.cover,\n                title = feedSubItem.title,\n                subTitle = feedSubItem.subTitle,\n                seasonId = feedSubItem.seasonId!!,\n                episodeId = feedSubItem.episodeId,\n                seasonType = SeasonIndexType.fromId(feedSubItem.seasonType!!),\n                rating = feedSubItem.rating ?: \"0\"\n            )\n        }\n\n        fun fromFeedSubItem(feedSubItem: dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedV3Data.FeedItem.FeedSubItem): PgcItem {\n            return PgcItem(\n                cover = feedSubItem.cover,\n                title = feedSubItem.title,\n                subTitle = feedSubItem.subTitle,\n                seasonId = feedSubItem.seasonId!!,\n                episodeId = feedSubItem.episodeId ?: feedSubItem.inline!!.epId,\n                seasonType = SeasonIndexType.fromId(feedSubItem.seasonType!!),\n                rating = feedSubItem.rating ?: \"0\"\n            )\n        }\n\n        fun fromIndexResultItem(indexResultItem: dev.aaa1115910.biliapi.http.entity.index.IndexResultData.IndexResultItem): PgcItem {\n            return PgcItem(\n                cover = indexResultItem.cover,\n                title = indexResultItem.title,\n                subTitle = indexResultItem.subTitle,\n                seasonId = indexResultItem.seasonId,\n                episodeId = indexResultItem.firstEp.epId,\n                seasonType = SeasonIndexType.fromId(indexResultItem.seasonType),\n                rating = indexResultItem.score\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/PgcType.kt",
    "content": "package dev.aaa1115910.biliapi.entity.pgc\n\nenum class PgcType {\n    Anime,\n    GuoChuang,\n    Movie,\n    Documentary,\n    Tv,\n    Variety\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/index/IndexParams.kt",
    "content": "package dev.aaa1115910.biliapi.entity.pgc.index\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\n\ninterface PgcIndexParam\n\n/**\n * 排序\n */\nenum class IndexOrder(val id: Int) : PgcIndexParam {\n    UpdateTime(0),      // 更新时间\n    DanmakuCount(1),    // 弹幕数量\n    PlayCount(2),       // 播放数量\n    FollowCount(3),     // 追番人数\n    Score(4),           // 最高评分\n    StartTime(5),       // 开播时间\n    PublishTime(6);     // 上映时间\n\n    companion object {\n        fun getList(pgcType: PgcType): List<IndexOrder> = when (pgcType) {\n            PgcType.Anime -> listOf(FollowCount, UpdateTime, Score, PlayCount, StartTime)\n            PgcType.GuoChuang -> listOf(FollowCount, UpdateTime, Score, PlayCount, StartTime)\n            PgcType.Movie -> listOf(PlayCount, UpdateTime, PublishTime, Score)\n            PgcType.Documentary -> listOf(PlayCount, Score, UpdateTime, PublishTime, DanmakuCount)\n            PgcType.Tv -> listOf(PlayCount, UpdateTime, DanmakuCount, Score, FollowCount)\n            PgcType.Variety -> listOf(PlayCount, UpdateTime, PublishTime, Score, DanmakuCount)\n        }\n    }\n}\n\nenum class IndexOrderType(val id: Int) : PgcIndexParam {\n    Desc(0),            // 降序\n    Asc(1);             // 升序\n}\n\n/**\n * 类型\n */\nenum class SeasonVersion(val id: Int) : PgcIndexParam {\n    All(-1),            // 全部\n    FeatureFilm(1),     // 正片\n    Movies(2),          // 电影\n    Other(3);           // 其他\n\n    companion object {\n        fun getList(pgcType: PgcType): List<SeasonVersion> {\n            return when (pgcType) {\n                PgcType.Anime, PgcType.GuoChuang -> listOf(All, FeatureFilm, Movies, Other)\n                else -> emptyList()\n            }\n        }\n    }\n}\n\n/**\n * 配音\n */\nenum class SpokenLanguage(val id: Int) : PgcIndexParam {\n    All(-1),            // 全部\n    OriginalSoundtrack(1),  // 原声\n    ChineseDubbing(2);  // 中文配音\n\n    companion object {\n        fun getList(pgcType: PgcType) = when (pgcType) {\n            PgcType.Anime -> listOf(All, OriginalSoundtrack, ChineseDubbing)\n            else -> emptyList()\n        }\n    }\n}\n\n/**\n * 地区\n */\nenum class Area(val id: Int) : PgcIndexParam {\n    All(-1),            // 全部\n    MainlandChina(1),   // 中国大陆\n    Japan(2),           // 日本\n    America(3),         // 美国\n    Britain(4),         // 英国\n    Other(5),           // 其他\n    ChinaHongKongTaiwan(6), // 中国港台 6,7\n    Korea(8),           // 韩国\n    France(9),          // 法国\n    Thailand(10),       // 泰国\n    Spain(13),          // 西班牙\n    Germany(15),        // 德国\n    Italy(35);          // 意大利\n\n    companion object {\n        fun getList(pgcType: PgcType) = when (pgcType) {\n            PgcType.Anime -> listOf(All, Japan, America, Other)\n            PgcType.Movie -> listOf(\n                All, MainlandChina, ChinaHongKongTaiwan, America, Japan, Korea, France,\n                Britain, Germany, Thailand, Italy, Spain, Other\n            )\n\n            PgcType.Tv -> listOf(All, MainlandChina, Japan, America, Britain, Other)\n            else -> emptyList()\n        }\n    }\n}\n\n/**\n * 状态（完结状态）\n */\nenum class IsFinish(val id: Int) : PgcIndexParam {\n    All(-1),            // 全部\n    Finished(1),        // 完结\n    Serialization(0);   // 连载\n\n    companion object {\n        fun getList(pgcType: PgcType) = when (pgcType) {\n            PgcType.Anime, PgcType.GuoChuang -> listOf(All, Finished, Serialization)\n            else -> emptyList()\n        }\n    }\n}\n\n/**\n * 版权\n */\nenum class Copyright(val id: Int) : PgcIndexParam {\n    All(-1),            // 全部\n    Exclusive(3),       // 独家\n    Other(1);           // 其他 1,2,4\n\n    companion object {\n        fun getList(pgcType: PgcType) = when (pgcType) {\n            PgcType.Anime, PgcType.GuoChuang -> listOf(All, Exclusive, Other)\n            else -> emptyList()\n        }\n    }\n}\n\n/**\n * 付费（付费状态）\n */\nenum class SeasonStatus(val id: Int) : PgcIndexParam {\n    All(-1),            // 全部\n    Free(1),            // 免费\n    Paid(2),            // 付费 2,6\n    Prime(4);           // 大会员 4,6\n\n    companion object {\n        fun getList(pgcType: PgcType) = when (pgcType) {\n            PgcType.Anime, PgcType.GuoChuang, PgcType.Movie -> listOf(All, Free, Paid, Prime)\n            PgcType.Documentary, PgcType.Tv, PgcType.Variety -> listOf(All, Free, Prime)\n        }\n    }\n}\n\n/**\n * 季度\n */\nenum class SeasonMonth(val id: Int) : PgcIndexParam {\n    All(-1),            // 全部\n    January(1),         // 1月\n    April(4),           // 4月\n    July(7),            // 7月\n    October(10);        // 10月\n\n    companion object {\n        fun getList(pgcType: PgcType) = when (pgcType) {\n            PgcType.Anime -> listOf(All, January, April, July, October)\n            else -> emptyList()\n        }\n    }\n}\n\n/**\n * 出品（方）\n */\nenum class Producer(val id: Int) : PgcIndexParam {\n    All(-1),            // 全部\n    BBC(1),             // BBC\n    NHK(2),             // NHK\n    SKY(3),             // SKY\n    CCTV(4),            // 央视\n    ITV(5),             // ITV\n    HistoryChannel(6),  // 历史频道\n    DiscoveryChannel(7),// 探索频道\n    SatelliteTV(8),     // 卫视\n    SelfMade(9),        // 自制\n    ZDF(10),            // ZDF\n    Cooperation(11),    // 合作机构\n    DomesticOther(12),  // 国内其他\n    ForeignOther(13),   // 国外其他\n    NationalGeographic(14), // 国家地理\n    Sony(15),           // 索尼\n    Universal(16),      // 环球\n    Paramount(17),      // 派拉蒙\n    Warner(18),         // 华纳\n    Disney(19),         // 迪士尼\n    HBO(20);            // HBO\n\n    companion object {\n        fun getList(pgcType: PgcType) = when (pgcType) {\n            PgcType.Documentary -> listOf(\n                All, CCTV, BBC, DiscoveryChannel, NationalGeographic, NHK, HistoryChannel,\n                SatelliteTV, SelfMade, ITV, SKY, ZDF, Cooperation, DomesticOther, ForeignOther,\n                Sony, Universal, Paramount, Warner, Disney, HBO\n            )\n\n            else -> emptyList()\n        }\n    }\n}\n\n/**\n * 年份（Year）\n */\nenum class Year(val str: String) : PgcIndexParam {\n    All(\"-1\"),          // 全部\n    Year2026(\"[2026,2027)\"), // 2026\n    Year2025(\"[2025,2026)\"), // 2025\n    Year2024(\"[2024,2025)\"), // 2024\n    Year2023(\"[2023,2024)\"), // 2023\n    Year2022(\"[2022,2023)\"), // 2022\n    Year2021(\"[2021,2022)\"), // 2021\n    Year2020(\"[2020,2021)\"), // 2020\n    Year2019(\"[2019,2020)\"), // 2019\n    Year2018(\"[2018,2019)\"), // 2018\n    Year2017(\"[2017,2018)\"), // 2017\n    Year2016(\"[2016,2017)\"), // 2016\n    Year2015(\"[2015,2016)\"), // 2015\n    Year2014_2010(\"[2010,2015)\"),   // 2014-2010\n    Year2009_2005(\"[2005,2010)\"),   // 2009-2005\n    Year2004_2000(\"[2000,2005)\"),   // 2004-2000\n    Year199x(\"[1990,2000)\"), // 90年代\n    Year198x(\"[1980,1990)\"), // 80年代\n    Earlier(\"[,1980)\"); // 更早\n\n    companion object {\n        fun getList(pgcType: PgcType) = when (pgcType) {\n            PgcType.Anime, PgcType.GuoChuang -> listOf(\n                All, Year2026, Year2025, Year2024, Year2023, Year2022, Year2021, Year2020,\n                Year2019, Year2018, Year2017, Year2016, Year2015, Year2014_2010,\n                Year2009_2005, Year2004_2000, Year199x, Year198x, Earlier\n            )\n\n            else -> emptyList()\n        }\n    }\n}\n\n/**\n * 年份（发布时间）\n */\nenum class ReleaseDate(val str: String) : PgcIndexParam {\n    All(\"-1\"),          // 全部\n    Year2026(\"[2026-01-01 00:00:00,2027-01-01 00:00:00)\"),  // 2026\n    Year2025(\"[2025-01-01 00:00:00,2026-01-01 00:00:00)\"),  // 2025\n    Year2024(\"[2024-01-01 00:00:00,2025-01-01 00:00:00)\"),  // 2024\n    Year2023(\"[2023-01-01 00:00:00,2024-01-01 00:00:00)\"),  // 2023\n    Year2022(\"[2022-01-01 00:00:00,2023-01-01 00:00:00)\"),  // 2022\n    Year2021(\"[2021-01-01 00:00:00,2022-01-01 00:00:00)\"),  // 2021\n    Year2020(\"[2020-01-01 00:00:00,2021-01-01 00:00:00)\"),  // 2020\n    Year2019(\"[2019-01-01 00:00:00,2020-01-01 00:00:00)\"),  // 2019\n    Year2018(\"[2018-01-01 00:00:00,2019-01-01 00:00:00)\"),  // 2018\n    Year2017(\"[2017-01-01 00:00:00,2018-01-01 00:00:00)\"),  // 2017\n    Year2016(\"[2016-01-01 00:00:00,2017-01-01 00:00:00)\"),  // 2016\n    Year2015_2010(\"[2010-01-01 00:00:00,2015-01-01 00:00:00)\"), // 2015-2010\n    Year2009_2005(\"[2005-01-01 00:00:00,2010-01-01 00:00:00)\"), // 2009-2005\n    Year2004_2000(\"[2000-01-01 00:00:00,2005-01-01 00:00:00)\"), // 2004-2000\n    Year199x(\"[1990-01-01 00:00:00,2000-01-01 00:00:00)\"),  // 90年代\n    Year198x(\"[1980-01-01 00:00:00,1990-01-01 00:00:00)\"),  // 80年代\n    Earlier(\"[,1980-01-01 00:00:00)\");  // 更早\n\n    companion object {\n        fun getList(pgcType: PgcType) = when (pgcType) {\n            PgcType.Movie, PgcType.Documentary, PgcType.Tv -> listOf(\n                All, Year2026, Year2025, Year2024, Year2023, Year2022, Year2021, Year2020,\n                Year2019, Year2018, Year2017, Year2016, Year2015_2010,\n                Year2009_2005, Year2004_2000, Year199x, Year198x, Earlier\n            )\n\n            else -> emptyList()\n        }\n    }\n}\n\n/**\n * 风格\n */\nenum class Style(val id: Int) : PgcIndexParam {\n    All(-1),            // 全部\n    Movie(-10),         // 电影\n\n    Original(10010),    // 原创\n    Comic(10011),       // 漫画改\n    Novel(10012),       // 小说改\n    Game(10013),        // 游戏改\n    Animation(10014),   // 动态漫\n    Puppetry(10015),    // 布袋戏\n    HotBlood(10016),    // 热血\n    TimeTravel(10017),  // 穿越\n    Fantasy(10018),     // 奇幻\n    XuanHuan(10019),    // 玄幻\n\n    Fight(10020),       // 战斗\n    Funny(10021),       // 搞笑\n    Daily(10022),       // 日常\n    ScienceFiction(10023),  // 科幻\n    Moe(10024),         // 萌系\n    Healing(10025),     // 治愈\n    School(10026),      // 校园\n    Children(10027),    // 少儿\n    InstantNoodles(10028),  // 泡面\n    InLove(10029),      // 恋爱\n\n    Girl(10030),        // 少女\n    Magic(10031),       // 魔法\n    Adventure(10032),   // 冒险\n    History(10033),     // 历史\n    Fiction(10034),     // 架空\n    Mecha(10035),       // 机战\n    GodDemon(10036),    // 神魔\n    VoiceControl(10037),// 声控\n    Sports(10038),      // 运动\n    Inspirational(10039),   // 励志\n\n    Music(10040),       // 音乐\n    Reasoning(10041),   // 推理\n    Club(10042),        // 社团\n    WisdomFight(10043), // 智斗\n    Tearjerker(10044),  // 催泪\n    Food(10045),        // 美食\n    Idol(10046),        // 偶像\n    Maiden(10047),      // 乙女\n    Workplace(10048),   // 职场\n    AncientStyle(10049),// 古风\n\n    Plot(10050),        // 剧情\n    Comedy(10051),      // 喜剧\n    Love(10052),        // 爱情\n    Action(10053),      // 动作\n    Terror(10054),      // 恐怖\n    Offense(10055),     // 犯罪\n    Thriller(10056),    // 惊悚\n    Suspense(10057),    // 悬疑\n    War(10058),         // 战争\n    // 10059\n\n    Biography(10060),   // 传记\n    Family(10061),      // 家庭\n    Opera(10062),       // 歌剧\n    Documentary(10063), // 纪实\n    Disaster(10064),    // 灾难\n    Humanities(10065),  // 人文\n    Technology(10066),  // 科技\n    Explore(10067),     // 探险\n    Universal(10068),   // 通用\n    CutePet(10069),     // 萌宠\n\n    Social(10070),      // 社会\n    Animal(10071),      // 动物\n    Nature(10072),      // 自然\n    Medical(10073),     // 医疗\n    Military(10074),    // 军事\n    Crime(10075),       // 罪案\n    Mystery(10076),     // 神秘\n    Travel(10077),      // 旅行\n    MartialArts(10078), // 武侠\n    Youth(10079),       // 青春\n\n    City(10080),        // 都市\n    AncientCostume(10081),  // 古装\n    SpyWar(10082),      // 谍战\n    Classic(10083),     // 经典\n    Emotion(10084),     // 情感\n    Myth(10085),        // 神话\n    Age(10086),         // 年代\n    Rural(10087),       // 农村\n    CriminalInvestigation(10088), // 刑侦\n    MilitaryLife(10089),// 军旅\n\n    Interview(10090),   // 访谈\n    TalkShow(10091),    // 脱口秀\n    RealityShow(10092), // 真人秀\n\n    //10093\n    Selection(10094),   // 选秀\n    Tourism(10095),     // 旅游\n    Concert(10096),     // 演唱会\n    ParentChild(10097), // 亲子\n    EveningParty(10098),// 晚会\n    Cultivate(10099),   // 养成\n\n    Culture(10100),     // 文化\n\n    //10101\n    SpecialEffects(10102),  // 特摄\n    ShortPlay(10103),    // 短剧\n    ShortFilm(10104);    // 短片\n\n    companion object {\n        fun getList(pgcType: PgcType) = when (pgcType) {\n            PgcType.Anime -> listOf(\n                All, Original, Comic, Novel, Game, SpecialEffects, Puppetry, HotBlood, TimeTravel,\n                Fantasy, Fight, Funny, Daily, ScienceFiction, Moe, Healing, School, Children,\n                InstantNoodles, InLove, Girl, Magic, Adventure, History, Fiction, Mecha, GodDemon,\n                VoiceControl, Sports, Inspirational, Music, Reasoning, Club, WisdomFight,\n                Tearjerker, Food, Idol, Maiden, Workplace\n            )\n\n            PgcType.GuoChuang -> listOf(\n                All, Original, Comic, Novel, Game, Animation, Puppetry, HotBlood, Fantasy, XuanHuan,\n                Fight, Funny, MartialArts, Daily, ScienceFiction, Moe, Healing, Suspense, School,\n                Children, InstantNoodles, InLove, Girl, Magic, History, Mecha, GodDemon,\n                VoiceControl, Sports, Inspirational, Music, Reasoning, Club, WisdomFight,\n                Tearjerker, Food, Idol, Maiden, Workplace, AncientStyle\n            )\n\n            PgcType.Movie -> listOf(\n                All, ShortFilm, Plot, Comedy, Love, Action, Terror, ScienceFiction, Offense,\n                Thriller, Suspense, Fantasy, War, Animation, Biography, Family, Opera, History,\n                Adventure, Documentary, Disaster, Comic, Novel\n            )\n\n            PgcType.Documentary -> listOf(\n                All, History, Food, Humanities, Technology, Explore, Universal, CutePet, Social,\n                Animal, Nature, Medical, Military, Disaster, Crime, Mystery, Travel, Sports, Movie\n            )\n\n            PgcType.Variety -> listOf(\n                All, Music, Interview, TalkShow, RealityShow, Selection, Food, Tourism,\n                EveningParty, Concert, Emotion, Comedy, ParentChild, Culture, Workplace,\n                CutePet, Cultivate\n            )\n\n            PgcType.Tv -> listOf(\n                All, Plot, Emotion, Funny, Suspense, City, Family, AncientCostume, History,\n                Fantasy, Youth, War, MartialArts, Inspirational, ShortPlay, ScienceFiction\n\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/index/PgcIndexCondition.kt",
    "content": "package dev.aaa1115910.biliapi.entity.pgc.index\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\nconst val PGC_INDEX_ORDER_FIELD = \"order\"\n\n@Serializable\ndata class PgcIndexConditionData(\n    @SerialName(\"filter\")\n    val filters: List<PgcIndexConditionFilter> = emptyList(),\n    val order: List<PgcIndexConditionOrder> = emptyList()\n) {\n    fun buildSections(): List<PgcIndexSection> = buildList {\n        if (order.isNotEmpty()) {\n            add(\n                PgcIndexSection(\n                    field = PGC_INDEX_ORDER_FIELD,\n                    title = \"排序\",\n                    options = order.map {\n                        PgcIndexOption(\n                            field = PGC_INDEX_ORDER_FIELD,\n                            keyword = it.field,\n                            name = it.name,\n                            sort = it.sort.substringBefore(',').ifBlank { \"0\" }\n                        )\n                    }\n                )\n            )\n        }\n        filters.forEach { filter ->\n            if (filter.values.isNotEmpty()) {\n                add(\n                    PgcIndexSection(\n                        field = filter.field,\n                        title = filter.name,\n                        options = filter.values.map { value ->\n                            PgcIndexOption(\n                                field = filter.field,\n                                keyword = value.keyword,\n                                name = value.name\n                            )\n                        }\n                    )\n                )\n            }\n        }\n    }\n}\n\n@Serializable\ndata class PgcIndexConditionFilter(\n    val field: String,\n    val name: String,\n    val values: List<PgcIndexConditionValue> = emptyList()\n)\n\n@Serializable\ndata class PgcIndexConditionOrder(\n    val field: String,\n    val name: String,\n    val sort: String = \"0\"\n)\n\n@Serializable\ndata class PgcIndexConditionValue(\n    val keyword: String,\n    val name: String\n)\n\ndata class PgcIndexSection(\n    val field: String,\n    val title: String,\n    val options: List<PgcIndexOption>\n)\n\ndata class PgcIndexOption(\n    val field: String,\n    val keyword: String,\n    val name: String,\n    val sort: String? = null\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/pgc/index/PgcIndexData.kt",
    "content": "package dev.aaa1115910.biliapi.entity.pgc.index\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcItem\n\ndata class PgcIndexData(\n    val list: List<PgcItem>,\n    val nextPage: PgcIndexPage\n) {\n    companion object {\n        fun fromIndexResultData(data: dev.aaa1115910.biliapi.http.entity.index.IndexResultData): PgcIndexData {\n            return PgcIndexData(\n                list = data.list.map { PgcItem.fromIndexResultItem(it) },\n                nextPage = PgcIndexPage(\n                    currentPage = data.num,\n                    pageSize = data.size,\n                    totalSize = data.total,\n                    nextPage = data.num + 1,\n                    hasNext = data.hasNext == 1\n                )\n            )\n        }\n    }\n\n    data class PgcIndexPage(\n        val currentPage: Int = 1,\n        val pageSize: Int = 20,\n        val totalSize: Int = 0,\n        val nextPage: Int = 1,\n        val hasNext: Boolean = true\n    )\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/rank/Popular.kt",
    "content": "package dev.aaa1115910.biliapi.entity.rank\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\n\ndata class PopularVideoData(\n    val list: List<UgcItem>,\n    val nextPage: PopularVideoPage,\n    val noMore: Boolean\n)\n\ndata class PopularVideoPage(\n    val nextWebPageSize: Int = 20,\n    val nextWebPageNumber: Int = 1,\n    val nextAppIndex: Int = 0,\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/reply/Comment.kt",
    "content": "package dev.aaa1115910.biliapi.entity.reply\n\nimport dev.aaa1115910.biliapi.entity.Picture\n\ndata class CommentsData(\n    val comments: List<Comment> = emptyList(),\n    val nextPage: CommentPage = CommentPage(),\n    val hasNext: Boolean\n) {\n    companion object {\n        fun fromCommentData(commentData: dev.aaa1115910.biliapi.http.entity.reply.CommentData): CommentsData {\n            val nextOffset = commentData.cursor.paginationReply?.nextOffset\n            return CommentsData(\n                comments = commentData.replies.map { Comment.fromReply(it) },\n                nextPage = CommentPage(\n                    nextWebPage = commentData.cursor.paginationReply?.nextOffset ?: \"\"\n                ),\n                hasNext = commentData.cursor.isEnd.not() && nextOffset != null\n            )\n        }\n\n        fun fromMainListReply(mainListReply: bilibili.main.community.reply.v1.MainListReply): CommentsData {\n            return CommentsData(\n                comments = mainListReply.repliesList.map { Comment.fromReplyInfo(it) },\n                nextPage = CommentPage(\n                    nextAppPage = mainListReply.paginationReply.nextOffset\n                ),\n                hasNext = mainListReply.cursor.isEnd.not()\n            )\n        }\n    }\n}\n\ndata class Comment(\n    val rpid: Long,\n    val mid: Long,\n    val oid: Long,\n    val type: Long,\n    val parent: Long,\n    val content: List<String>,\n    val member: Member,\n    val timeDesc: String,\n    val emotes: List<Emote>,\n    val pictures: List<Picture>,\n    val replies: List<Comment>,\n    val repliesCount: Int,\n    val like: Long = 0,\n) {\n    companion object {\n        fun fromReply(reply: dev.aaa1115910.biliapi.http.entity.reply.CommentData.Reply): Comment {\n            return Comment(\n                rpid = reply.rpid,\n                mid = reply.mid,\n                oid = reply.oid,\n                type = reply.type,\n                parent = reply.parent,\n                content = reply.content.message.splitWithEmotes(*reply.content.emote.keys.toTypedArray()),\n                member = Member(\n                    mid = reply.mid,\n                    avatar = reply.member.avatar,\n                    name = reply.member.uname\n                ),\n                timeDesc = reply.replyControl.timeDesc,\n                emotes = reply.content.emote.values.map { Emote.fromEmote(it) },\n                pictures = reply.content.pictures.map { Picture.fromPicture(it) },\n                replies = reply.replies.map { fromReply(it) },\n                repliesCount = reply.rcount,\n                like = reply.like.toLong()\n            )\n        }\n\n        fun fromReplyInfo(reply: bilibili.main.community.reply.v1.ReplyInfo): Comment {\n            return Comment(\n                rpid = reply.id,\n                mid = reply.mid,\n                oid = reply.oid,\n                type = reply.type,\n                parent = reply.parent,\n                content = reply.content.message.splitWithEmotes(*reply.content.emoteMap.keys.toTypedArray()),\n                member = Member(\n                    mid = reply.mid,\n                    avatar = reply.member.face,\n                    name = reply.member.name\n                ),\n                timeDesc = reply.replyControl.timeDesc,\n                emotes = reply.content.emoteMap.values.map { Emote.fromEmote(it) },\n                pictures = reply.content.picturesList.map { Picture.fromPicture(it) },\n                replies = reply.repliesList.map { fromReplyInfo(it) },\n                repliesCount = reply.count.toInt(),\n                like = reply.like\n            )\n        }\n    }\n\n    data class Member(\n        val mid: Long,\n        val avatar: String,\n        val name: String\n    )\n\n    data class Emote(\n        val text: String,\n        val url: String,\n        val size: EmoteSize\n    ) {\n        companion object {\n            fun fromEmote(emote: dev.aaa1115910.biliapi.http.entity.reply.CommentData.Reply.Content.Emote): Emote {\n                return Emote(\n                    text = emote.text,\n                    url = emote.url,\n                    size = if (emote.meta.size == 1) EmoteSize.Small else EmoteSize.Large\n                )\n            }\n\n            fun fromEmote(emote: bilibili.main.community.reply.v1.Emote): Emote {\n                return Emote(\n                    text = emote.text,\n                    url = emote.url,\n                    size = if (emote.size == 1L) EmoteSize.Small else EmoteSize.Large\n                )\n            }\n        }\n    }\n}\n\nenum class EmoteSize(val fontSize: Int) {\n    Small(20), Large(20)\n}\n\nprivate fun String.splitWithEmotes(vararg emotes: String): List<String> {\n    val delimiter = emotes.joinToString(\"|\").replace(\"[\", \"\\\\[\").replace(\"]\", \"\\\\]\")\n    val regex = Regex(\"(?=$delimiter)|(?<=$delimiter)\")\n    return this.split(regex)\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/reply/CommentPage.kt",
    "content": "package dev.aaa1115910.biliapi.entity.reply\n\ndata class CommentPage(\n    val nextWebPage: String = \"\",\n    val nextAppPage: String = \"\"\n)\n\ndata class CommentReplyPage(\n    val nextWebPage: Int = 1,\n    val nextAppPage: String = \"\"\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/reply/CommentRepliesData.kt",
    "content": "package dev.aaa1115910.biliapi.entity.reply\n\ndata class CommentRepliesData(\n    val rootComment: Comment,\n    val replies: List<Comment>,\n    val nextPage: CommentReplyPage = CommentReplyPage(),\n    val hasNext: Boolean\n) {\n    companion object {\n        fun fromCommentReplyData(commentReplyData: dev.aaa1115910.biliapi.http.entity.reply.CommentReplyData): CommentRepliesData {\n            val nextOffset = commentReplyData.page.num * commentReplyData.page.size\n            return CommentRepliesData(\n                rootComment = Comment.fromReply(commentReplyData.root),\n                replies = commentReplyData.replies.map { Comment.fromReply(it) },\n                nextPage = CommentReplyPage(\n                    nextWebPage = commentReplyData.page.num + 1\n                ),\n                hasNext = commentReplyData.page.count > nextOffset\n            )\n        }\n\n        fun fromCommentReplyList(detailListReply: bilibili.main.community.reply.v1.DetailListReply): CommentRepliesData {\n            return CommentRepliesData(\n                rootComment = Comment.fromReplyInfo(detailListReply.root),\n                replies = detailListReply.root.repliesList.map { Comment.fromReplyInfo(it) },\n                nextPage = CommentReplyPage(\n                    nextAppPage = detailListReply.paginationReply.nextOffset\n                ),\n                hasNext = detailListReply.cursor.isEnd.not()\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/reply/CommentSort.kt",
    "content": "package dev.aaa1115910.biliapi.entity.reply\n\nenum class CommentSort(val param: Int) {\n    Hot(3), HotAndTime(1), Time(2)\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/search/Hotword.kt",
    "content": "package dev.aaa1115910.biliapi.entity.search\n\n\ndata class Hotword(\n    val keyword: String,\n    val showName: String,\n    val icon: String?,\n) {\n    companion object {\n        fun fromHttpWebHotword(hotword: dev.aaa1115910.biliapi.http.entity.search.Hotword) =\n            Hotword(\n                keyword = hotword.keyword,\n                showName = hotword.showName,\n                icon = hotword.icon\n            )\n\n        fun fromHttpAppSquareDataItem(squareDataItem: dev.aaa1115910.biliapi.http.entity.search.AppSearchSquareData.SquareData.SquareDataItem) =\n            Hotword(\n                keyword = squareDataItem.keyword ?: \"\",\n                showName = squareDataItem.showName ?: \"\",\n                icon = squareDataItem.icon\n            )\n\n        fun fromHttpAppSearchTrendingHotword(hotword: dev.aaa1115910.biliapi.http.entity.search.SearchTendingData.Hotword) =\n            Hotword(\n                keyword = hotword.keyword,\n                showName = hotword.showName,\n                icon = hotword.icon\n            )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/season/FollowingSeasons.kt",
    "content": "package dev.aaa1115910.biliapi.entity.season\n\ndata class FollowingSeasonData(\n    val list: List<FollowingSeason>,\n    val total: Int\n)\n\ndata class FollowingSeason(\n    val seasonId: Int,\n    val title: String,\n    val cover: String\n) {\n    companion object {\n        fun fromFollowingSeason(season: dev.aaa1115910.biliapi.http.entity.season.WebFollowingSeason) =\n            FollowingSeason(\n                seasonId = season.seasonId,\n                title = season.title,\n                cover = season.cover\n            )\n\n        fun fromFollowingSeason(season: dev.aaa1115910.biliapi.http.entity.season.AppFollowingSeason) =\n            FollowingSeason(\n                seasonId = season.seasonId,\n                title = season.title,\n                cover = season.cover\n            )\n    }\n}\n\nenum class FollowingSeasonType(val id: Int, val paramName: String) {\n    Bangumi(id = 1, paramName = \"bangumi\"),\n    Cinema(id = 2, paramName = \"cinema\")\n}\n\nenum class FollowingSeasonStatus(val id: Int) {\n    All(id = 0), Want(id = 1), Watching(id = 2), Watched(id = 3)\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/season/IndexResult.kt",
    "content": "package dev.aaa1115910.biliapi.entity.season\n\ndata class IndexResultData(\n    val list: List<IndexResultItem>,\n    val nextPage: IndexResultPage\n) {\n    companion object {\n        fun fromIndexResultData(data: dev.aaa1115910.biliapi.http.entity.index.IndexResultData) =\n            IndexResultData(\n                list = data.list.map { IndexResultItem.fromIndexResultItem(it) },\n                nextPage = IndexResultPage(\n                    nextPage = (data.num + 1).takeIf { data.hasNext == 1 } ?: -1,\n                    hasNext = data.hasNext == 1\n                )\n            )\n    }\n}\n\ndata class IndexResultPage(\n    val nextPage: Int = 1,\n    val hasNext: Boolean = true\n)\n\ndata class IndexResultItem(\n    val title: String,\n    val subTitle: String,\n    val cover: String,\n    val score: String,\n    val badge: Badge?,\n    val indexShow: String,\n    val seasonId: Int\n) {\n    companion object {\n        fun fromIndexResultItem(item: dev.aaa1115910.biliapi.http.entity.index.IndexResultData.IndexResultItem): IndexResultItem {\n            return IndexResultItem(\n                title = item.title,\n                subTitle = item.subTitle,\n                cover = item.cover,\n                score = item.score,\n                badge = Badge(\n                    text = item.badgeInfo.text,\n                    bgColor = item.badgeInfo.bgColor,\n                    bgColorNight = item.badgeInfo.bgColorNight\n                ).takeIf { item.badgeInfo.text.isNotEmpty() },\n                indexShow = item.indexShow,\n                seasonId = item.seasonId\n            )\n        }\n    }\n\n    data class Badge(\n        val text: String,\n        val bgColor: String,\n        val bgColorNight: String\n    )\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/season/Timeline.kt",
    "content": "package dev.aaa1115910.biliapi.entity.season\n\nimport java.util.Date\n\nenum class TimelineFilter(\n    val webFilterId: Int,\n    val appFilterId: Int\n) {\n    All(webFilterId = -1, appFilterId = 0),\n    Anime(webFilterId = 1, appFilterId = 1),\n    Following(webFilterId = -1, appFilterId = 2),\n    GuoChuang(webFilterId = 4, appFilterId = 3);\n\n    companion object {\n        val webFilters = listOf(Anime, GuoChuang)\n        val appFilters = listOf(All, Anime, Following, GuoChuang)\n    }\n}\n\ndata class Timeline(\n    val dateString: String,\n    val date: Date,\n    val dayOfWeek: Int,\n    val isToday: Boolean,\n    val episodes: List<TimelineEp>\n) {\n    companion object {\n        fun fromTimeline(timeline: dev.aaa1115910.biliapi.http.entity.video.Timeline) = Timeline(\n            dateString = timeline.date,\n            date = Date(timeline.dateTs * 1000L),\n            dayOfWeek = timeline.dayOfWeek,\n            isToday = timeline.isToday,\n            episodes = timeline.episodes.map { TimelineEp.fromTimelineEpisode(it) }\n        )\n    }\n}\n\ndata class TimelineEp(\n    val cover: String,\n    val title: String,\n    val seasonId: Int,\n    val publishIndex: String,\n    val publishTime: String,\n    val publishDate: Date\n) {\n    companion object {\n        fun fromTimelineEpisode(episode: dev.aaa1115910.biliapi.http.entity.video.Timeline.Episode) =\n            TimelineEp(\n                cover = episode.cover,\n                title = episode.title,\n                seasonId = episode.seasonId,\n                publishIndex = episode.pubIndex,\n                publishTime = episode.pubTime,\n                publishDate = Date(episode.pubTs * 1000L)\n            )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/ugc/UgcItem.kt",
    "content": "package dev.aaa1115910.biliapi.entity.ugc\n\nimport dev.aaa1115910.biliapi.http.entity.home.RcmdIndexData\nimport dev.aaa1115910.biliapi.http.entity.home.RcmdTopData\nimport dev.aaa1115910.biliapi.util.convertStringTimeToSeconds\nimport java.text.SimpleDateFormat\nimport java.util.Calendar\nimport java.util.Locale\nimport java.util.TimeZone\n\ndata class UgcItem(\n    val aid: Long,\n    val bvid: String = \"\",\n    val title: String,\n    val cover: String,\n    val author: String,\n    val authorId: Long = 0,\n    val authorFace: String = \"\",\n    val play: Long,\n    val danmaku: Int,\n    val duration: Int,\n    val idx: Int = -1,\n    val pubTime: String? = null,\n) {\n    companion object {\n        fun fromRcmdItem(rcmdItem: RcmdIndexData.RcmdItem) =\n            UgcItem(\n                aid = rcmdItem.args.aid ?: 0,\n                title = rcmdItem.title!!,\n                cover = rcmdItem.cover!!,\n                author = rcmdItem.args.upName ?: \"\",\n                authorId = rcmdItem.args.upId ?: 0,\n//                authorFace = rcmdItem.args.upFace ?: \"\",\n                play = with(rcmdItem.coverLeftText1) {\n                    runCatching {\n                        if (this!!.endsWith(\"万\")) {\n                            (this.substring(0, this.length - 1).toDouble() * 10000).toLong()\n                        } else {\n                            this.toLong()\n                        }\n                    }.getOrDefault(-1)\n                },\n                danmaku = with(rcmdItem.coverLeftText2) {\n                    if (this == null) return@with -1\n                    runCatching {\n                        if (this.endsWith(\"万\")) {\n                            (this.substring(0, this.length - 1).toDouble() * 10000).toInt()\n                        } else {\n                            this.toInt()\n                        }\n                    }.getOrDefault(-1)\n                },\n                duration = rcmdItem.coverRightText?.convertStringTimeToSeconds() ?: 0,\n                idx = rcmdItem.idx\n            )\n\n        fun fromRcmdItem(rcmdItem: RcmdTopData.RcmdItem) =\n            UgcItem(\n                aid = rcmdItem.id,\n                bvid = rcmdItem.bvid,\n                title = rcmdItem.title,\n                cover = rcmdItem.pic,\n                author = rcmdItem.owner?.name ?: \"\",\n                authorId = rcmdItem.owner?.mid ?: 0,\n                authorFace = rcmdItem.owner?.face ?: \"\",\n                play = rcmdItem.stat?.view ?: -1L,\n                danmaku = rcmdItem.stat?.danmaku ?: -1,\n                duration = rcmdItem.duration,\n                pubTime = rcmdItem.pubdate.smartDate\n            )\n\n        fun fromVideoInfo(videoInfo: dev.aaa1115910.biliapi.http.entity.video.VideoInfo) =\n            UgcItem(\n                aid = videoInfo.aid,\n                title = videoInfo.title,\n                duration = videoInfo.duration,\n                author = videoInfo.owner.name,\n                authorId = videoInfo.owner.mid,\n                authorFace = videoInfo.owner.face,\n                cover = videoInfo.pic,\n                play = videoInfo.stat.view,\n                danmaku = videoInfo.stat.danmaku,\n                pubTime = videoInfo.pubdate.smartDate\n            )\n\n        fun fromSmallCoverV5(card: bilibili.app.card.v1.SmallCoverV5): UgcItem {\n            // 格式：\"n.n万观看 · n天前\"\n            val playAndPubTime = card.rightDesc2.split(\" · \")\n            val play = playAndPubTime.getOrNull(0)?.let { convertPlayStringToLong(it) } ?: -1\n            val pubTime = playAndPubTime.getOrNull(1)\n\n            return UgcItem(\n                aid = card.base.param.toLong(),\n                title = card.base.title,\n                duration = convertStringTimeToSeconds(card.coverRightText1),\n                author = card.rightDesc1,\n//                authorId = card.base.upId,\n//                authorFace = card.base.upFace,\n                cover = card.base.cover,\n                play = play,\n                pubTime = pubTime,\n                danmaku = -1,\n                idx = card.base.idx.toInt()\n            )\n        }\n\n        fun fromRegionDynamicListItem(item: dev.aaa1115910.biliapi.http.entity.region.RegionDynamicList.Item) =\n            UgcItem(\n                aid = item.param.toLong(),\n                title = item.title,\n                duration = item.duration,\n                author = item.name,\n//                authorId = item.mid,\n                authorFace = item.face,\n                cover = item.cover,\n                play = item.play ?: -1,\n                danmaku = item.danmaku ?: -1,\n                pubTime = item.pubDate.smartDate\n            )\n\n        fun fromRegionRcmdArchive(archive: dev.aaa1115910.biliapi.http.entity.region.RegionFeedRcmd.Archive) =\n            UgcItem(\n                aid = archive.aid,\n                title = archive.title,\n                duration = archive.duration,\n                author = archive.author.name,\n                authorId = archive.author.mid,\n                cover = archive.cover,\n                play = archive.stat.view,\n                danmaku = archive.stat.danmaku,\n                pubTime = archive.pubdate.toSmartDate()\n            )\n    }\n}\n\nprivate fun convertPlayStringToLong(text: String): Long {\n    if (text.isBlank()) return -1\n\n    val value = text.replace(\"观看\", \"\").trim()\n\n    return try {\n        when {\n            value.endsWith(\"万\") -> {\n                val num = value.removeSuffix(\"万\").toDouble()\n                (num * 10_000).toLong()\n            }\n\n            value.endsWith(\"亿\") -> {\n                val num = value.removeSuffix(\"亿\").toDouble()\n                (num * 100_000_000).toLong()\n            }\n\n            else -> {\n                value.toLong()\n            }\n        }\n    } catch (e: Exception) {\n        -1\n    }\n}\nprivate fun convertStringTimeToSeconds(time: String): Int {\n    val parts = time.split(\":\")\n    val hours = if (parts.size == 3) parts[0].toInt() else 0\n    val minutes = parts[parts.size - 2].toInt()\n    val seconds = parts[parts.size - 1].toInt()\n    return (hours * 3600) + (minutes * 60) + seconds\n}\n\n/**\n * 智能日期格式化 (兼容低版本 Android)\n * @param timeZone 时区 (默认系统时区)\n */\nfun Long.toSmartDate(timeZone: TimeZone = TimeZone.getDefault()): String? {\n    if (this <= 0) return null\n    try {\n        // 自动识别秒级或毫秒级时间戳\n        // 秒级时间戳通常小于等于10位数，目前直到2286年都是10位数\n        // 毫秒级时间戳通常为13位数\n        val timeInMillis = if (this < 10000000000L) this * 1000L else this\n\n        // 创建日历实例\n        val cal = Calendar.getInstance(timeZone).apply {\n            this.timeInMillis = timeInMillis\n        }\n\n        // 获取当前年份\n        val currentYear = Calendar.getInstance(timeZone).get(Calendar.YEAR)\n\n        // 动态格式选择\n        val pattern = if (cal.get(Calendar.YEAR) == currentYear) {\n            \"M-d H:mm\"\n        } else {\n            \"yyyy-M-d\"\n        }\n\n        // 线程安全的日期格式化\n        return SimpleDateFormat(pattern, Locale.CHINESE).apply {\n            this.timeZone = timeZone\n        }.format(cal.time)\n    } catch (e: Exception) {\n        return null\n    }\n}\n\nval Int.smartDate: String?\n    get() = this.toLong().toSmartDate()"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/ugc/UgcType.kt",
    "content": "package dev.aaa1115910.biliapi.entity.ugc\n\n@Deprecated(\"Use dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2 instead\")\nenum class UgcType(val rid: Int, val codename: String, val locId: Int = -1) {\n    Douga(1, \"douga\", 4973),\n    DougaMad(24, \"mad\"),\n    DougaMmd(25, \"mmd\"),\n    DougaHandDrawn(47, \"handdrawn\"),\n    DougaVoice(257, \"voice\"),\n    DougaGarageKit(210, \"garage_kit\"),\n    DougaTokusatsu(86, \"tokusatsu\"),\n    DougaAcgnTalks(253, \"acgntalks\"),\n    DougaOther(27, \"other\"),\n\n    Game(4, \"game\", 4991),\n    GameStandAlone(17, \"stand_alone\"),\n    GameESports(171, \"esports\"),\n    GameMobile(172, \"mobile\"),\n    GameOnline(65, \"online\"),\n    GameBoard(173, \"board\"),\n    GameGmv(121, \"gmv\"),\n    GameMusic(136, \"music\"),\n    GameMugen(19, \"mugen\"),\n\n    Kichiku(119, \"kichiku\", 5004),\n    KichikuGuide(22, \"guide\"),\n    KichikuMad(26, \"mad\"),\n    KichikuManualVocaloid(126, \"manual_vocaloid\"),\n    KichikuTheatre(216, \"theatre\"),\n    KichikuCourse(127, \"course\"),\n\n    Music(3, \"music\", 4979),\n    MusicOriginal(28, \"original\"),\n    MusicLive(29, \"live\"),\n    MusicCover(31, \"cover\"),\n    MusicPerform(31, \"perform\"),\n    MusicCommentary(243, \"commentary\"),\n    MusicVocaloidUtau(30, \"vocaloid\"),\n    MusicMv(193, \"mv\"),\n    MusicFanVideos(266, \"fan_videos\"),\n    MusicAiMusic(265, \"ai_music\"),\n    MusicRadio(267, \"radio\"),\n    MusicTutorial(244, \"tutorial\"),\n    MusicOther(130, \"other\"),\n\n    Dance(129, \"dance\", 4985),\n    DanceOtaku(20, \"otaku\"),\n    DanceHiphop(198, \"hiphop\"),\n    DanceStar(199, \"star\"),\n    DanceChina(200, \"china\"),\n    DanceGestures(255, \"gestures\"),\n    DanceThreeD(154, \"three_d\"),\n    DanceDemo(156, \"demo\"),\n\n    Cinephile(181, \"cinephile\", 5008),\n    CinephileCinecism(182, \"cinecism\"),\n    CinephileNibtage(183, \"montage\"),\n    CinephileMashup(260, \"mashup\"),\n    CinephileAiImagine(259, \"ai_imaging\"),\n    CinephileTrailerInfo(184, \"trailer_info\"),\n    CinephileShortPlay(85, \"shortplay\"),\n    CinephileShortFilm(256, \"shortfilm\"),\n    CinephileComperhensive(261, \"comprehensive\"),\n\n    Ent(5, \"ent\", 5007),\n    EntTalker(241, \"talker\"),\n    EntCpRecommendation(262, \"cp_recommendation\"),\n    EntBeauty(263, \"beauty\"),\n    EntFans(242, \"fans\"),\n    EntEntertainmentNews(264, \"entertainment_news\"),\n    EntCelebrity(137, \"celebrity\"),\n    EntVariety(71, \"variety\"),\n\n    Knowledge(36, \"knowledge\", 4997),\n    KnowledgeScience(201, \"science\"),\n    KnowledgeSocialScience(124, \"social_science\"),\n    KnowledgeHumanity(228, \"humanity_history\"),\n    KnowledgeBusiness(207, \"business\"),\n    KnowledgeCampus(208, \"campus\"),\n    KnowledgeCareer(209, \"career\"),\n    KnowledgeDesign(229, \"design\"),\n    KnowledgeSkill(122, \"skill\"),\n\n    Tech(188, \"tech\", 4998),\n    TechDigital(95, \"digital\"),\n    TechApplication(230, \"application\"),\n    TechComputerTech(231, \"computer_tech\"),\n    TechIndustry(232, \"industry\"),\n    TechDiy(233, \"diy\"),\n\n    Information(202, \"information\", 5005),\n    InformationHotspot(203, \"hotspot\"),\n    InformationGlobal(204, \"global\"),\n    InformationSocial(205, \"social\"),\n    InformationMultiple(206, \"multiple\"),\n\n    Food(211, \"food\", 5002),\n    FoodMake(76, \"make\"),\n    FoodDetective(212, \"detective\"),\n    FoodMeasurement(213, \"measurement\"),\n    FoodRural(214, \"rural\"),\n    FoodRecord(215, \"record\"),\n\n    Life(160, \"life\", 5001),\n    LifeFunny(138, \"funny\"),\n    LifeParenting(254, \"parenting\"),\n    LifeTravel(250, \"travel\"),\n    LiseRuralLife(251, \"rurallife\"),\n    LifeHome(239, \"home\"),\n    LifeHandMake(161, \"handmake\"),\n    LifePainting(162, \"painting\"),\n    LifeDaily(21, \"daily\"),\n\n    Car(223, \"car\", 5000),\n    CarKnowledge(258, \"knowledge\"),\n    CarStrategy(227, \"strategy\"),\n    CarNewEnergyVehicle(247, \"newenergyvehicle\"),\n    CarRacing(245, \"racing\"),\n    CarModifiedVehicle(246, \"modifiedvehicle\"),\n    CarMotorcycle(240, \"motorcycle\"),\n    CarTouringCar(248, \"touringcar\"),\n    CarLife(176, \"life\"),\n\n    Fashion(155, \"fashion\", 5006),\n    FashionMakeup(157, \"makeup\"),\n    FashionCos(252, \"cos\"),\n    FashionClothing(158, \"clothing\"),\n    FashionCatwalk(159, \"catwalk\"),\n\n    Sports(234, \"sports\", 4999),\n    SportsBasketball(235, \"basketball\"),\n    SportsFootball(249, \"football\"),\n    SportsAerobics(164, \"aerobics\"),\n    SportsAthletic(236, \"athletic\"),\n    SportsCulture(237, \"culture\"),\n    SportsComprehensive(238, \"comprehensive\"),\n\n    Animal(217, \"animal\", 5003),\n    AnimalCat(218, \"cat\"),\n    AnimalDog(291, \"dog\"),\n    AnimalReptiles(222, \"reptiles\"),\n    AnimalWildAnima(221, \"wild_animal\"),\n    AnimalSecondEdition(220, \"second_edition\"),\n    AnimalComposite(75, \"animal_composite\")\n\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/ugc/UgcTypeV2.kt",
    "content": "package dev.aaa1115910.biliapi.entity.ugc\n\nenum class UgcTypeV2(val tid: Int, val codename: String, val channelId: Int? = null) {\n    // 动画\n    Douga(1005, \"douga\", 7),\n    DougaFanAnime(2037, \"fan_anime\"),\n    DougaGarageKit(2038, \"garage_kit\"),\n    DougaCosplay(2039, \"cosplay\"),\n    DougaOffline(2040, \"offline\"),\n    DougaEditing(2041, \"editing\"),\n    DougaCommentary(2042, \"commentary\"),\n    DougaQuickView(2043, \"quick_view\"),\n    DougaVoice(2044, \"voice\"),\n    DougaInformation(2045, \"information\"),\n    DougaInterpret(2046, \"interpret\"),\n    DougaVup(2047, \"vup\"),\n    DougaTokusatsu(2048, \"tokusatsu\"),\n    DougaPuppetry(2049, \"puppetry\"),\n    DougaComic(2050, \"comic\"),\n    DougaMotion(2051, \"motion\"),\n    DougaReaction(2052, \"reaction\"),\n    DougaTutorial(2053, \"tutorial\"),\n    DougaOther(2054, \"other\"),\n\n    // 游戏\n    Game(1008, \"game\", 8),\n    GameRpg(2064, \"rpg\"),\n    GameMmorpg(2065, \"mmorpg\"),\n    GameStandAlone(2066, \"stand_alone\"),\n    GameSlg(2067, \"slg\"),\n    GameTbs(2068, \"tbs\"),\n    GameRts(2069, \"rts\"),\n    GameMoba(2070, \"moba\"),\n    GameStg(2071, \"stg\"),\n    GameSpg(2072, \"spg\"),\n    GameAct(2073, \"act\"),\n    GameMsc(2074, \"msc\"),\n    GameSim(2075, \"sim\"),\n    GameOtome(2076, \"otome\"),\n    GamePuz(2077, \"puz\"),\n    GameSandbox(2078, \"sandbox\"),\n    GameOther(2079, \"other\"),\n\n    // 鬼畜\n    Kichiku(1007, \"kichiku\", 9),\n    KichikuGuide(2059, \"guide\"),\n    KichikuTheatre(2060, \"theatre\"),\n    KichikuManualVocaloid(2061, \"manual_vocaloid\"),\n    KichikuMad(2062, \"mad\"),\n    KichikuOther(2063, \"other\"),\n\n    // 音乐\n    Music(1003, \"music\", 10),\n    MusicOriginal(2016, \"original\"),\n    MusicMv(2017, \"mv\"),\n    MusicLive(2018, \"live\"),\n    MusicFanVideos(2019, \"fan_videos\"),\n    MusicCover(2020, \"cover\"),\n    MusicPerform(2021, \"perform\"),\n    MusicVocaloid(2022, \"vocaloid\"),\n    MusicAiMusic(2023, \"ai_music\"),\n    MusicRadio(2024, \"radio\"),\n    MusicTutorial(2025, \"tutorial\"),\n    MusicCommentary(2026, \"commentary\"),\n    MusicOther(2027, \"other\"),\n\n    // 舞蹈\n    Dance(1004, \"dance\", 11),\n    DanceOtaku(2028, \"otaku\"),\n    DanceHiphop(2029, \"hiphop\"),\n    DanceGestures(2030, \"gestures\"),\n    DanceStar(2031, \"star\"),\n    DanceChina(2032, \"china\"),\n    DanceTutorial(2033, \"tutorial\"),\n    DanceBallet(2034, \"ballet\"),\n    DanceWota(2035, \"wota\"),\n    DanceOther(2036, \"other\"),\n\n    // 影视\n    Cinephile(1001, \"cinephile\", 12),\n    CinephileCommentary(2001, \"commentary\"),\n    CinephileMontage(2002, \"montage\"),\n    CinephileInformation(2003, \"information\"),\n    CinephilePorterage(2004, \"porterage\"),\n    CinephileShortFilm(2005, \"shortfilm\"),\n    CinephileAi(2006, \"ai\"),\n    CinephileReaction(2007, \"reaction\"),\n    CinephileOther(2008, \"other\"),\n\n    // 娱乐\n    Ent(1002, \"ent\", 13),\n    EntCommentary(2009, \"commentary\"),\n    EntMontage(2010, \"montage\"),\n    EntFansVideo(2011, \"fans_video\"),\n    EntInformation(2012, \"information\"),\n    EntReaction(2013, \"reaction\"),\n    EntVariety(2014, \"variety\"),\n    EntOther(2015, \"other\"),\n\n    // 知识\n    Knowledge(1010, \"knowledge\", 14),\n    KnowledgeExam(2084, \"exam\"),\n    KnowledgeLangSkill(2085, \"lang_skill\"),\n    KnowledgeCampus(2086, \"campus\"),\n    KnowledgeBusiness(2087, \"business\"),\n    KnowledgeSocialObservation(2088, \"social_observation\"),\n    KnowledgePolitics(2089, \"politics\"),\n    KnowledgeHumanityHistory(2090, \"humanity_history\"),\n    KnowledgeDesign(2091, \"design\"),\n    KnowledgePsychology(2092, \"psychology\"),\n    KnowledgeCareer(2093, \"career\"),\n    KnowledgeScience(2094, \"science\"),\n    KnowledgeOther(2095, \"other\"),\n\n    // 科技数码\n    Tech(1012, \"tech\", 15),\n    TechComputer(2099, \"computer\"),\n    TechPhone(2100, \"phone\"),\n    TechPad(2101, \"pad\"),\n    TechPhotography(2102, \"photography\"),\n    TechMachine(2103, \"machine\"),\n    TechCreate(2104, \"create\"),\n    TechOther(2105, \"other\"),\n\n    // 资讯\n    Information(1009, \"information\", 16),\n    InformationPolitics(2080, \"politics\"),\n    InformationOverseas(2081, \"overseas\"),\n    InformationSocial(2082, \"social\"),\n    InformationOther(2083, \"other\"),\n\n    // 美食\n    Food(1020, \"food\", 17),\n    FoodMake(2149, \"make\"),\n    FoodDetective(2150, \"detective\"),\n    FoodCommentary(2151, \"commentary\"),\n    FoodRecord(2152, \"record\"),\n    FoodOther(2153, \"other\"),\n\n    // 小剧场\n    Shortplay(1021, \"shortplay\", 18),\n    ShortplayPlot(2154, \"plot\"),\n    ShortplayLang(2155, \"lang\"),\n    ShortplayUpVariety(2156, \"up_variety\"),\n    ShortplayInterview(2157, \"interview\"),\n\n    // 汽车\n    Car(1013, \"car\", 19),\n    CarCommentary(2106, \"commentary\"),\n    CarCulture(2107, \"culture\"),\n    CarLife(2108, \"life\"),\n    CarTech(2109, \"tech\"),\n    CarOther(2110, \"other\"),\n\n    // 时尚美妆\n    Fashion(1014, \"fashion\", 20),\n    FashionMakeup(2111, \"makeup\"),\n    FashionSkincare(2112, \"skincare\"),\n    FashionCos(2113, \"cos\"),\n    FashionOutfits(2114, \"outfits\"),\n    FashionAccessories(2115, \"accessories\"),\n    FashionJewelry(2116, \"jewelry\"),\n    FashionTrick(2117, \"trick\"),\n    FashionCommentary(2118, \"commentary\"),\n    FashionOther(2119, \"other\"),\n\n    // 体育运动\n    Sports(1018, \"sports\", 21),\n    SportsTrend(2133, \"trend\"),\n    SportsFootball(2134, \"football\"),\n    SportsBasketball(2135, \"basketball\"),\n    SportsRunning(2136, \"running\"),\n    SportsKungfu(2137, \"kungfu\"),\n    SportsFighting(2138, \"fighting\"),\n    SportsBadminton(2139, \"badminton\"),\n    SportsInformation(2140, \"information\"),\n    SportsMatch(2141, \"match\"),\n    SportsOther(2142, \"other\"),\n\n    // 动物\n    Animal(1024, \"animal\", 22),\n    AnimalCat(2167, \"cat\"),\n    AnimalDog(2168, \"dog\"),\n    AnimalReptiles(2169, \"reptiles\"),\n    AnimalScience(2170, \"science\"),\n    AnimalOther(2171, \"other\"),\n\n    // vlog\n    Vlog(1029, \"vlog\", 23),\n    VlogLife(2194, \"life\"),\n    VlogStudent(2195, \"student\"),\n    VlogCareer(2196, \"career\"),\n    VlogOther(2197, \"other\"),\n\n    // 绘画\n    Painting(1006, \"painting\", 24),\n    PaintingAcg(2055, \"acg\"),\n    PaintingNoneAcg(2056, \"none_acg\"),\n    PaintingTutorial(2057, \"tutorial\"),\n    PaintingOther(2058, \"other\"),\n\n    // 人工智能\n    Ai(1011, \"ai\", 25),\n    AiTutorial(2096, \"tutorial\"),\n    AiInformation(2097, \"information\"),\n    AiOther(2098, \"other\"),\n\n    // 家装房产\n    Home(1015, \"home\", 26),\n    HomeTrade(2120, \"trade\"),\n    HomeRenovation(2121, \"renovation\"),\n    HomeFurniture(2122, \"furniture\"),\n    HomeAppliances(2123, \"appliances\"),\n\n    // 户外潮流\n    Outdoors(1016, \"outdoors\", 27),\n    OutdoorsCamping(2124, \"camping\"),\n    OutdoorsHiking(2125, \"hiking\"),\n    OutdoorsExplore(2126, \"explore\"),\n    OutdoorsOther(2127, \"other\"),\n\n    // 健身\n    Gym(1017, \"gym\", 28),\n    GymScience(2128, \"science\"),\n    GymTutorial(2129, \"tutorial\"),\n    GymRecord(2130, \"record\"),\n    GymFigure(2131, \"figure\"),\n    GymOther(2132, \"other\"),\n\n    // 手工\n    Handmake(1019, \"handmake\", 29),\n    HandmakeHandbook(2143, \"handbook\"),\n    HandmakeLight(2144, \"light\"),\n    HandmakeTraditional(2145, \"traditional\"),\n    HandmakeRelief(2146, \"relief\"),\n    HandmakeDiy(2147, \"diy\"),\n    HandmakeOther(2148, \"other\"),\n\n    // 旅游出行\n    Travel(1022, \"travel\", 30),\n    TravelRecord(2158, \"record\"),\n    TravelStrategy(2159, \"strategy\"),\n    TravelCity(2160, \"city\"),\n    TravelTransport(2161, \"transport\"),\n\n    // 三农\n    Rural(1023, \"rural\", 31),\n    RuralPlanting(2162, \"planting\"),\n    RuralFishing(2163, \"fishing\"),\n    RuralHarvest(2164, \"harvest\"),\n    RuralTech(2165, \"tech\"),\n    RuralLife(2166, \"life\"),\n\n    // 亲子\n    Parenting(1025, \"parenting\", 32),\n    ParentingPregnantCare(2172, \"pregnant_care\"),\n    ParentingInfantCare(2173, \"infant_care\"),\n    ParentingTalent(2174, \"talent\"),\n    ParentingCute(2175, \"cute\"),\n    ParentingInteraction(2176, \"interaction\"),\n    ParentingEducation(2177, \"education\"),\n    ParentingOther(2178, \"other\"),\n\n    // 健康\n    Health(1026, \"health\", 33),\n    HealthScience(2179, \"science\"),\n    HealthRegimen(2180, \"regimen\"),\n    HealthSexes(2181, \"sexes\"),\n    HealthPsychology(2182, \"psychology\"),\n    HealthAsmr(2183, \"asmr\"),\n    HealthOther(2184, \"other\"),\n\n    // 情感\n    Emotion(1027, \"emotion\", 34),\n    EmotionFamily(2185, \"family\"),\n    EmotionRomantic(2186, \"romantic\"),\n    EmotionInterpersonal(2187, \"interpersonal\"),\n    EmotionGrowth(2188, \"growth\"),\n\n    // 生活兴趣\n    LifeJoy(1030, \"life_joy\", 35),\n    LifeJoyLeisure(2198, \"leisure\"),\n    LifeJoyOnSite(2199, \"on_site\"),\n    LifeJoyArtisticProducts(2200, \"artistic_products\"),\n    LifeJoyTrendyToys(2201, \"trendy_toys\"),\n    LifeJoyOther(2202, \"other\"),\n\n    // 生活经验\n    LifeExperience(1031, \"life_experience\", 36),\n    LifeExperienceSkills(2203, \"skills\"),\n    LifeExperienceProcedures(2204, \"procedures\"),\n    LifeExperienceMarriage(2205, \"marriage\"),\n\n    // 神秘学\n    Mysticism(1028, \"mysticism\", 44),\n    MysticismTarot(2189, \"tarot\"),\n    MysticismHoroscope(2190, \"horoscope\"),\n    MysticismMetaphysics(2191, \"metaphysics\"),\n    MysticismHealing(2192, \"healing\"),\n    MysticismOther(2193, \"other\");\n\n    companion object {\n        val dougaList = listOf(\n            DougaFanAnime, DougaGarageKit, DougaCosplay, DougaOffline, DougaEditing,\n            DougaCommentary, DougaQuickView, DougaVoice, DougaInformation, DougaInterpret,\n            DougaVup, DougaTokusatsu, DougaPuppetry, DougaComic, DougaMotion, DougaReaction,\n            DougaTutorial, DougaOther\n        )\n        val gameList = listOf(\n            GameRpg, GameMmorpg, GameStandAlone, GameSlg, GameTbs, GameRts, GameMoba, GameStg,\n            GameSpg, GameAct, GameMsc, GameSim, GameOtome, GamePuz, GameSandbox, GameOther\n        )\n        val kichikuList = listOf(\n            KichikuGuide, KichikuTheatre, KichikuManualVocaloid, KichikuMad, KichikuOther\n        )\n        val musicList = listOf(\n            MusicOriginal, MusicMv, MusicLive, MusicFanVideos, MusicCover, MusicPerform,\n            MusicVocaloid, MusicAiMusic, MusicRadio, MusicTutorial, MusicCommentary, MusicOther\n        )\n        val danceList = listOf(\n            DanceOtaku, DanceHiphop, DanceGestures, DanceStar, DanceChina,\n            DanceTutorial, DanceBallet, DanceWota, DanceOther\n        )\n        val cinephileList = listOf(\n            CinephileCommentary, CinephileMontage, CinephileInformation, CinephilePorterage,\n            CinephileShortFilm, CinephileAi, CinephileReaction, CinephileOther\n        )\n        val entList = listOf(\n            EntCommentary, EntMontage, EntFansVideo, EntInformation, EntReaction, EntVariety,\n            EntOther\n        )\n        val knowledgeList = listOf(\n            KnowledgeExam, KnowledgeLangSkill, KnowledgeCampus, KnowledgeBusiness,\n            KnowledgeSocialObservation, KnowledgePolitics, KnowledgeHumanityHistory,\n            KnowledgeDesign, KnowledgePsychology, KnowledgeCareer, KnowledgeScience,\n            KnowledgeOther\n        )\n        val techList = listOf(\n            TechComputer, TechPhone, TechPad, TechPhotography, TechMachine, TechCreate, TechOther\n        )\n        val informationList = listOf(\n            InformationPolitics, InformationOverseas, InformationSocial, InformationOther\n        )\n        val foodList = listOf(\n            FoodMake, FoodDetective, FoodCommentary, FoodRecord, FoodOther\n        )\n        val shortplayList = listOf(\n            ShortplayPlot, ShortplayLang, ShortplayUpVariety, ShortplayInterview\n        )\n        val carList = listOf(\n            CarCommentary, CarCulture, CarLife, CarTech, CarOther\n        )\n        val fashionList = listOf(\n            FashionMakeup, FashionSkincare, FashionCos, FashionOutfits, FashionAccessories,\n            FashionJewelry, FashionTrick, FashionCommentary, FashionOther\n        )\n        val sportsList = listOf(\n            SportsTrend, SportsFootball, SportsBasketball, SportsRunning, SportsKungfu,\n            SportsFighting, SportsBadminton, SportsInformation, SportsMatch, SportsOther\n        )\n        val animalList = listOf(\n            AnimalCat, AnimalDog, AnimalReptiles, AnimalScience, AnimalOther\n        )\n        val vlogList = listOf(\n            VlogLife, VlogStudent, VlogCareer, VlogOther\n        )\n        val paintingList = listOf(\n            PaintingAcg, PaintingNoneAcg, PaintingTutorial, PaintingOther\n        )\n        val aiList = listOf(\n            AiTutorial, AiInformation, AiOther\n        )\n        val homeList = listOf(\n            HomeTrade, HomeRenovation, HomeFurniture, HomeAppliances\n        )\n        val outdoorsList = listOf(\n            OutdoorsCamping, OutdoorsHiking, OutdoorsExplore, OutdoorsOther\n        )\n        val gymList = listOf(\n            GymScience, GymTutorial, GymRecord, GymFigure, GymOther\n        )\n        val handmakeList = listOf(\n            HandmakeHandbook, HandmakeLight, HandmakeTraditional, HandmakeRelief, HandmakeDiy,\n            HandmakeOther\n        )\n        val travelList = listOf(\n            TravelRecord, TravelStrategy, TravelCity, TravelTransport\n        )\n        val ruralList = listOf(\n            RuralPlanting, RuralFishing, RuralHarvest, RuralTech, RuralLife\n        )\n        val parentingList = listOf(\n            ParentingPregnantCare, ParentingInfantCare, ParentingTalent, ParentingCute,\n            ParentingInteraction, ParentingEducation, ParentingOther\n        )\n        val healthList = listOf(\n            HealthScience, HealthRegimen, HealthSexes, HealthPsychology, HealthAsmr,\n            HealthOther\n        )\n        val emotionList = listOf(\n            EmotionFamily, EmotionRomantic, EmotionInterpersonal, EmotionGrowth\n        )\n        val lifeJoyList = listOf(\n            LifeJoyLeisure, LifeJoyOnSite, LifeJoyArtisticProducts, LifeJoyTrendyToys, LifeJoyOther\n        )\n        val lifeExperienceList = listOf(\n            LifeExperienceSkills, LifeExperienceProcedures, LifeExperienceMarriage\n        )\n        val mysticismList = listOf(\n            MysticismTarot, MysticismHoroscope, MysticismMetaphysics, MysticismHealing,\n            MysticismOther\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/ugc/region/UgcFeedData.kt",
    "content": "package dev.aaa1115910.biliapi.entity.ugc.region\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\n\ndata class UgcFeedData(\n    var hasNext: Boolean,\n    var nextPage: UgcFeedPage,\n    var items: List<UgcItem> = emptyList()\n) {\n    companion object {\n        fun fromRegionFeedRcmd(data: dev.aaa1115910.biliapi.http.entity.region.RegionFeedRcmd): UgcFeedData {\n            return UgcFeedData(\n                hasNext = data.archives.isNotEmpty(),\n                nextPage = UgcFeedPage(),\n                items = data.archives.map { UgcItem.fromRegionRcmdArchive(it) }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/ugc/region/UgcFeedPage.kt",
    "content": "package dev.aaa1115910.biliapi.entity.ugc.region\n\ndata class UgcFeedPage(\n    val nextPage: Int = 1\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/ugc/region/UgcRegionData.kt",
    "content": "package dev.aaa1115910.biliapi.entity.ugc.region\n\nimport dev.aaa1115910.biliapi.entity.CarouselData\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\n\n@Deprecated(\"User region v2 instead\")\ndata class UgcRegionData(\n    val carouselData: CarouselData?,\n    val items: List<UgcItem>,\n    val next: UgcRegionPage\n) {\n    companion object {\n        fun fromRegionDynamic(data: dev.aaa1115910.biliapi.http.entity.region.RegionDynamic): UgcRegionData {\n            return UgcRegionData(\n                carouselData = data.banner?.let { CarouselData.fromUgcRegionDynamicBanner(it) },\n                items = data.new.map { UgcItem.fromRegionDynamicListItem(it) },\n                next = UgcRegionPage(data.cBottom)\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/ugc/region/UgcRegionListData.kt",
    "content": "package dev.aaa1115910.biliapi.entity.ugc.region\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\n\n@Deprecated(\"User region v2 instead\")\ndata class UgcRegionListData(\n    val items: List<UgcItem>,\n    val next: UgcRegionPage\n) {\n    companion object {\n        fun fromRegionDynamicList(data: dev.aaa1115910.biliapi.http.entity.region.RegionDynamicList): UgcRegionListData {\n            return UgcRegionListData(\n                items = data.new.map { UgcItem.fromRegionDynamicListItem(it) },\n                next = UgcRegionPage(data.cBottom)\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/ugc/region/UgcRegionPage.kt",
    "content": "package dev.aaa1115910.biliapi.entity.ugc.region\n\n@Deprecated(\"User region v2 instead\")\ndata class UgcRegionPage(\n    val nextPage: Long = 0\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/user/Author.kt",
    "content": "package dev.aaa1115910.biliapi.entity.user\n\nimport dev.aaa1115910.biliapi.http.entity.video.VideoOwner\n\ndata class Author(\n    val mid: Long,\n    val name: String,\n    val face: String\n) {\n    companion object {\n        fun fromVideoOwner(videoOwner: VideoOwner) = Author(\n            mid = videoOwner.mid,\n            name = videoOwner.name,\n            face = videoOwner.face\n        )\n\n        fun fromAuthor(author: bilibili.app.archive.v1.Author) = Author(\n            mid = author.mid,\n            name = author.name,\n            face = author.face\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/user/Dynamic.kt",
    "content": "package dev.aaa1115910.biliapi.entity.user\n\nimport bilibili.app.dynamic.v2.DynModuleType\nimport bilibili.app.dynamic.v2.Module\nimport bilibili.app.dynamic.v2.ModuleDynamic.ModuleItemCase\nimport bilibili.app.dynamic.v2.Paragraph\nimport bilibili.app.dynamic.v2.VideoType\nimport dev.aaa1115910.biliapi.entity.Picture\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonElement\nimport okhttp3.internal.toLongOrDefault\n\ndata class DynamicData(\n    val dynamics: List<DynamicItem>,\n    val hasMore: Boolean,\n    val historyOffset: String,\n    val updateBaseline: String\n) {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n        private val availableDynamicTypes = listOf(\n            DynamicType.Av,\n            DynamicType.Draw,\n            DynamicType.Forward,\n            DynamicType.Word,\n            DynamicType.LiveRcmd,\n            DynamicType.Pgc,\n            DynamicType.Article,\n            DynamicType.None\n        )\n        private val availableWebDynamicTypes = availableDynamicTypes.map { it.webValue }\n        private val availableAppDynamicTypes = availableDynamicTypes.map { it.appValue }\n\n        fun fromDynamicData(data: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicData) =\n            DynamicData(\n                dynamics = data.items\n                    .mapNotNull {\n                        if (!availableWebDynamicTypes.contains(it.type)) {\n                            logger.warn { \"unknown dynamic type ${it.type}, up: ${it.modules.moduleAuthor.name}, date: ${it.modules.moduleAuthor.pubTime}\" }\n                            return@mapNotNull null\n                        }\n\n                        if (it.type == DynamicType.Forward.webValue) {\n                            if (!availableWebDynamicTypes.contains(it.orig?.type)) {\n                                logger.warn { \"unknown dynamic forward type ${it.orig?.type}, up: ${it.modules.moduleAuthor.name}, date: ${it.modules.moduleAuthor.pubTime}\" }\n                                return@mapNotNull null\n                            }\n                        }\n\n                        DynamicItem.fromDynamicItem(it)\n                    },\n                hasMore = data.hasMore,\n                historyOffset = data.offset,\n                updateBaseline = data.updateBaseline\n            ).also {\n                logger.info { \"updateBaseline: ${data.updateBaseline}\" }\n                logger.info { \"offset: ${data.offset}\" }\n            }\n\n        fun fromDynamicData(data: bilibili.app.dynamic.v2.DynAllReply) = DynamicData(\n            dynamics = data.dynamicList.listList\n                .mapNotNull {\n                    if (!availableAppDynamicTypes.contains(it.cardType)) {\n                        logger.warn { \"unknown dynamic type ${it.cardType.name}, up: ${it.getAuthorModule()?.author?.name}, date: ${it.getAuthorModule()?.ptimeLabelText}\" }\n                        return@mapNotNull null\n                    }\n\n                    if (it.cardType == bilibili.app.dynamic.v2.DynamicType.forward) {\n                        // source not exist\n                        if (it.getItemNullModule() != null) {\n                            return@mapNotNull DynamicItem.fromDynamicItem(it)\n                        } else if (!availableAppDynamicTypes.contains(it.getDynamicModule()?.dynForward?.item?.cardType)) {\n                            logger.warn { \"unknown dynamic forward type ${it.getDynamicModule()?.dynForward?.item?.cardType}, up: ${it.getAuthorModule()?.author?.name}, date: ${it.getAuthorModule()?.ptimeLabelText}\" }\n                            return@mapNotNull null\n                        }\n                    }\n\n                    DynamicItem.fromDynamicItem(it)\n                },\n            hasMore = data.dynamicList.hasMore,\n            historyOffset = data.dynamicList.historyOffset,\n            updateBaseline = data.dynamicList.updateBaseline\n        ).also {\n            logger.info { \"updateBaseline: ${data.dynamicList.updateBaseline}\" }\n            logger.info { \"historyOffset: ${data.dynamicList.historyOffset}\" }\n        }\n    }\n}\n\ndata class DynamicItem(\n    var id: String? = null,\n    var commentId: Long = 0,\n    var commentType: Long = 0,\n    var type: DynamicType,\n    val author: DynamicAuthorModule,\n    var video: DynamicVideoModule? = null,\n    var draw: DynamicDrawModule? = null,\n    var word: DynamicWordModule? = null,\n    var liveRcmd: DynamicLiveRcmdModule? = null,\n    var pgc: DynamicPgcModule? = null,\n    var article: DynamicArticleModule? = null,\n    var none: DynamicNoneModule? = null,\n    val footer: DynamicFooterModule? = null,\n    var orig: DynamicItem? = null,\n    var jumpUrl: String? = null\n) {\n    companion object {\n        fun fromDynamicItem(item: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem): DynamicItem {\n            val dynamicType = DynamicType.fromWebValue(item.type)\n            val dynamicItem = DynamicItem(\n                id = item.idStr,\n                commentId = item.basic.commentIdStr.toLongOrDefault(0),\n                commentType = item.basic.commentType,\n                type = dynamicType,\n                author = DynamicAuthorModule.fromModuleAuthor(item.modules.moduleAuthor),\n                footer = DynamicFooterModule.fromModuleStat(item.modules.moduleStat)\n            )\n            when (dynamicType) {\n                DynamicType.Av -> dynamicItem.video =\n                    DynamicVideoModule.fromModuleArchive(item.modules.moduleDynamic.major!!.archive!!)\n\n                DynamicType.UgcSeason -> TODO()\n                DynamicType.Forward -> dynamicItem.apply {\n                    word = DynamicWordModule.fromModuleDynamic(item.modules.moduleDynamic)\n                    orig = fromDynamicItem(item.orig!!)\n                    jumpUrl = item.orig?.basic?.jumpUrl\n                }\n\n                DynamicType.Word -> dynamicItem.word =\n                    DynamicWordModule.fromModuleDynamic(item.modules.moduleDynamic)\n\n                DynamicType.Draw -> dynamicItem.draw =\n                    DynamicDrawModule.fromModuleDynamic(item.modules.moduleDynamic)\n\n                DynamicType.LiveRcmd -> dynamicItem.liveRcmd =\n                    DynamicLiveRcmdModule.fromModuleDynamic(item.modules.moduleDynamic)\n\n                DynamicType.Pgc -> dynamicItem.pgc =\n                    DynamicPgcModule.fromModulePgc(item.modules.moduleDynamic.major!!.pgc!!)\n\n                DynamicType.Article -> dynamicItem.article =\n                    DynamicArticleModule.fromModuleArticle(item.modules.moduleDynamic.major!!.article!!)\n\n                DynamicType.None -> dynamicItem.none =\n                    DynamicNoneModule.fromModuleDynamic(item.modules.moduleDynamic.major!!.none!!)\n            }\n            return dynamicItem\n        }\n\n        fun fromDynamicItem(\n            item: bilibili.app.dynamic.v2.DynamicItem,\n            isForwardItem: Boolean = false\n        ): DynamicItem {\n            val dynamicType = DynamicType.fromAppValue(item.cardType)\n            val commentType: Long = when (item.cardType) {\n                bilibili.app.dynamic.v2.DynamicType.av,\n                bilibili.app.dynamic.v2.DynamicType.pgc -> 1\n\n                bilibili.app.dynamic.v2.DynamicType.draw -> 11\n\n                bilibili.app.dynamic.v2.DynamicType.forward,\n                bilibili.app.dynamic.v2.DynamicType.word,\n                bilibili.app.dynamic.v2.DynamicType.article,\n                bilibili.app.dynamic.v2.DynamicType.live_rcmd -> 17\n\n                else -> 17\n            }\n            val dynamicItem = DynamicItem(\n                id = item.extend.dynIdStr,\n                commentId = item.extend.businessId.toLongOrDefault(0),\n                commentType = commentType,\n                // item.extend.rType 总是为 0\n                //commentType = item.extend.rType,\n                type = dynamicType,\n                author = if (dynamicType == DynamicType.None) {\n                    DynamicAuthorModule(\"\", \"\", -1, \"\", \"\")\n                } else if (isForwardItem) {\n                    DynamicAuthorModule.fromExtendAndModuleAuthorForward(\n                        item.extend, item.getAuthorModuleForward()!!\n                    )\n                } else {\n                    DynamicAuthorModule.fromModuleAuthor(item.getAuthorModule()!!)\n                },\n                video = item.getDynamicModule()?.let {\n                    DynamicVideoModule.fromModuleArchive(it.dynArchive).apply {\n                        text = item.getDescModule()?.text ?: \"\"\n                    }\n                },\n                footer = if (!isForwardItem) {\n                    // 获取动态详情时 module_list 中没有 module_stat，但 module_bottom 中包含了 module_stat\n                    DynamicFooterModule.fromModuleStat(\n                        item.getStatModule() ?: item.getBottomModule()!!.moduleStat\n                    )\n                } else null\n            )\n\n            when (dynamicType) {\n                DynamicType.Av -> dynamicItem.video = item.getDynamicModule()?.let {\n                    DynamicVideoModule.fromModuleArchive(it.dynArchive).apply {\n                        text = item.getDescModule()?.text ?: \"\"\n                    }\n                }\n\n                DynamicType.UgcSeason -> TODO()\n                DynamicType.Draw -> dynamicItem.draw =\n                    item.getOpusSummaryModule()?.let { opusSummaryModule ->\n                        DynamicDrawModule.fromModuleOpusSummaryAndModuleDynamic(\n                            opusSummaryModule,\n                            item.getDynamicModule()\n                        )\n                    } ?: let {\n                        DynamicDrawModule.fromModuleDescAndModuleDynamic(\n                            item.getParagraphModule(),\n                            item.getDescModule()!!,\n                            item.getDynamicModule()\n                        )\n                    }\n\n\n                DynamicType.Word -> dynamicItem.word =\n                    item.getOpusSummaryModule()?.let { opusSummaryModule ->\n                        DynamicWordModule.fromModuleOpusSummary(opusSummaryModule)\n                    } ?: let {\n                        DynamicWordModule.fromModuleDesc(item.getDescModule()!!)\n                    }\n\n                DynamicType.Forward -> dynamicItem.apply {\n                    word = DynamicWordModule.fromModuleDesc(item.getDescModule()!!)\n                    val item2 = item.getDynamicModule()?.dynForward?.item\n                    if (item2 == null) {\n                        println()\n                        val emptyDynamic = bilibili.app.dynamic.v2.dynamicItem {\n                            cardType = bilibili.app.dynamic.v2.DynamicType.dyn_none\n                            modules.addAll(item.modulesList)\n                        }\n                        orig = fromDynamicItem(emptyDynamic, true)\n                    } else {\n                        orig = fromDynamicItem(item2!!, true)\n                    }\n                }\n\n                DynamicType.LiveRcmd -> dynamicItem.liveRcmd =\n                    DynamicLiveRcmdModule.fromModuleDynamic(item.getDynamicModule()!!)\n\n                DynamicType.Pgc -> dynamicItem.pgc =\n                    DynamicPgcModule.fromModulePgc(item.getDynamicModule()!!.dynPgc)\n\n                DynamicType.Article -> dynamicItem.article =\n                    DynamicArticleModule.fromModuleArticle(item.getDynamicModule()!!.dynArticle)\n\n                DynamicType.None -> dynamicItem.none =\n                    DynamicNoneModule.fromModuleDynamic(item.getItemNullModule()!!)\n            }\n\n            return dynamicItem\n        }\n    }\n\n    data class DynamicAuthorModule(\n        val author: String,\n        val avatar: String,\n        val mid: Long,\n        val pubTime: String,\n        val pubAction: String\n    ) {\n        companion object {\n            fun fromModuleAuthor(moduleAuthor: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Author) =\n                DynamicAuthorModule(\n                    author = moduleAuthor.name,\n                    avatar = moduleAuthor.face,\n                    mid = moduleAuthor.mid,\n                    pubTime = moduleAuthor.pubTime,\n                    pubAction = moduleAuthor.pubAction\n                )\n\n            fun fromModuleAuthor(moduleAuthor: bilibili.app.dynamic.v2.ModuleAuthor) =\n                DynamicAuthorModule(\n                    author = moduleAuthor.author.name,\n                    avatar = moduleAuthor.author.face,\n                    mid = moduleAuthor.author.mid,\n                    pubTime = moduleAuthor.ptimeLabelText,\n                    pubAction = \"\"\n                )\n\n            fun fromExtendAndModuleAuthorForward(\n                extend: bilibili.app.dynamic.v2.Extend,\n                moduleAuthorForward: bilibili.app.dynamic.v2.ModuleAuthorForward\n            ) =\n                DynamicAuthorModule(\n                    author = extend.origName,\n                    avatar = extend.origFace,\n                    mid = extend.uid,\n                    pubTime = moduleAuthorForward.ptimeLabelText,\n                    pubAction = \"\"\n                )\n        }\n    }\n\n    data class DynamicVideoModule(\n        val aid: Long,\n        val bvid: String? = null,\n        val cid: Long,\n        val epid: Int? = null,\n        val seasonId: Int? = null,\n        val title: String,\n        var text: String,\n        val cover: String,\n        val duration: String,\n        val play: String,\n        val danmaku: String,\n        val isChargingArc: Boolean = false,\n        val chargingArcBadge: String = \"\"\n    ) {\n        companion object {\n            fun fromModuleArchive(moduleArchive: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Dynamic.Major.Archive): DynamicVideoModule {\n                val isChargingArc = moduleArchive.badge.text.contains(\"充电\") || moduleArchive.badge.text.contains(\"限时免费\")\n                return DynamicVideoModule(\n                    aid = moduleArchive.aid.toLong(),\n                    bvid = moduleArchive.bvid,\n                    cid = 0,\n                    title = moduleArchive.title,\n                    text = moduleArchive.desc,\n                    cover = moduleArchive.cover,\n                    duration = moduleArchive.durationText,\n                    play = moduleArchive.stat.play,\n                    danmaku = moduleArchive.stat.danmaku,\n                    isChargingArc = isChargingArc,\n                    chargingArcBadge = if (isChargingArc) moduleArchive.badge.text else \"\"\n                )\n            }\n\n            fun fromModuleArchive(moduleArchive: bilibili.app.dynamic.v2.MdlDynArchive): DynamicVideoModule {\n                val badgeText = moduleArchive.badgeList.firstOrNull()?.text ?: \"\"\n                val isChargingArc = badgeText.contains(\"充电\") || badgeText.contains(\"限时免费\")\n                return DynamicVideoModule(\n                    aid = moduleArchive.avid,\n                    bvid = moduleArchive.bvid,\n                    cid = moduleArchive.cid,\n                    epid = moduleArchive.episodeId.toInt(),\n                    seasonId = moduleArchive.pgcSeasonId.toInt(),\n                    title = moduleArchive.title,\n                    text = \"\",\n                    cover = moduleArchive.cover,\n                    duration = moduleArchive.coverLeftText1,\n                    play = moduleArchive.coverLeftText2,\n                    danmaku = moduleArchive.coverLeftText3,\n                    isChargingArc = isChargingArc,\n                    chargingArcBadge = if (isChargingArc) badgeText else \"\"\n                )\n            }\n        }\n    }\n\n    data class DynamicFooterModule(\n        val like: Int,\n        val comment: Int,\n        val share: Int\n    ) {\n        companion object {\n            fun fromModuleStat(moduleStat: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Stat?) =\n                moduleStat?.let {\n                    DynamicFooterModule(\n                        like = moduleStat.like.count,\n                        comment = moduleStat.comment.count,\n                        share = moduleStat.forward.count\n                    )\n                }\n\n            fun fromModuleStat(moduleStat: bilibili.app.dynamic.v2.ModuleStat) =\n                DynamicFooterModule(\n                    like = moduleStat.like.toInt(),\n                    comment = moduleStat.reply.toInt(),\n                    share = moduleStat.repost.toInt()\n                )\n\n            fun fromModuleBottom(moduleButtom: bilibili.app.dynamic.v2.ModuleButtom) =\n                fromModuleStat(moduleButtom.moduleStat)\n        }\n    }\n\n    data class DynamicDrawModule(\n        val title: String?,\n        val text: String,\n        val images: List<Picture>\n    ) {\n        companion object {\n            fun fromModuleDynamic(moduleDynamic: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Dynamic) =\n                DynamicDrawModule(\n                    title = null,\n                    text = moduleDynamic.desc?.text\n                        ?: moduleDynamic.major?.opus?.summary?.text\n                        ?: \"empty text\",\n                    images = (moduleDynamic.major?.draw?.items?.map(Picture::fromPicture)\n                        ?: moduleDynamic.major?.opus?.pics?.map(Picture::fromPicture))\n                        ?.distinctBy { it.url }\n                        ?: emptyList()\n                )\n\n            fun fromModuleOpusSummaryAndModuleDynamic(\n                moduleOpusSummary: bilibili.app.dynamic.v2.ModuleOpusSummary,\n                moduleDynamic: bilibili.app.dynamic.v2.ModuleDynamic?\n            ): DynamicDrawModule {\n                var title = \"\"\n                var text = \"\"\n                val images = mutableListOf<Picture>()\n\n                when (val titleContentType = moduleOpusSummary.title.contentCase) {\n                    Paragraph.ContentCase.TEXT -> title = moduleOpusSummary.title.text.nodesList\n                        .joinToString(\"\") { it.rawText }\n\n                    else -> println(\"not implemented: ModuleOpusSummary titleContentType: $titleContentType\")\n                }\n\n                when (val summaryContentType = moduleOpusSummary.summary.contentCase) {\n                    Paragraph.ContentCase.TEXT -> text = moduleOpusSummary.summary.text.nodesList\n                        .joinToString(\"\") { it.rawText }\n\n                    else -> println(\"not implemented: ModuleOpusSummary summaryContentType: $summaryContentType\")\n                }\n\n                when (val dynamicItemType = moduleDynamic?.moduleItemCase) {\n                    null -> println(\"ModuleDynamic is null\")\n                    ModuleItemCase.DYN_DRAW -> images.addAll(\n                        moduleDynamic.dynDraw.itemsList.map(Picture::fromPicture)\n                    )\n\n                    else -> println(\"not implemented: ModuleOpusSummary dynamicItemType $dynamicItemType\")\n                }\n\n                return DynamicDrawModule(\n                    title = title,\n                    text = text,\n                    images = images.distinctBy { it.url }\n                )\n            }\n\n            fun fromModuleDescAndModuleDynamic(\n                moduleParagraph: bilibili.app.dynamic.v2.ModuleParagraph?,\n                moduleDesc: bilibili.app.dynamic.v2.ModuleDesc,\n                moduleDynamic: bilibili.app.dynamic.v2.ModuleDynamic?\n            ): DynamicDrawModule {\n                var title = \"\"\n                var text = \"\"\n                val images = mutableListOf<Picture>()\n\n                text = moduleDesc.descList.joinToString(\"\") { it.text }\n\n                if (moduleParagraph != null && moduleParagraph.isArticleTitle) {\n\n                    when (val titleContentType = moduleParagraph.paragraph.contentCase) {\n                        Paragraph.ContentCase.TEXT -> title =\n                            moduleParagraph.paragraph.text.nodesList\n                                .joinToString(\"\") { it.rawText }\n\n                        else -> println(\"not implemented: ModuleOpusSummary titleContentType: $titleContentType\")\n                    }\n                }\n\n                when (val dynamicItemType = moduleDynamic?.moduleItemCase) {\n                    null -> println(\"ModuleDynamic is null\")\n                    ModuleItemCase.DYN_DRAW -> images.addAll(\n                        moduleDynamic.dynDraw.itemsList.map(Picture::fromPicture)\n                    )\n\n                    else -> println(\"not implemented: ModuleOpusSummary dynamicItemType $dynamicItemType\")\n                }\n\n                return DynamicDrawModule(\n                    title = title,\n                    text = text,\n                    images = images.distinctBy { it.url }\n                )\n            }\n        }\n    }\n\n    data class DynamicWordModule(\n        val text: String\n    ) {\n        companion object {\n            fun fromModuleDynamic(moduleDynamic: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Dynamic) =\n                DynamicWordModule(\n                    text = moduleDynamic.major?.opus?.summary?.text\n                        ?: moduleDynamic.desc?.text\n                        ?: \"empty content\"\n                )\n\n            fun fromModuleOpusSummary(moduleOpusSummary: bilibili.app.dynamic.v2.ModuleOpusSummary) =\n                DynamicWordModule(\n                    text = moduleOpusSummary.summary.text.nodesList\n                        .joinToString(\"\") { it.rawText }\n                )\n\n            fun fromModuleDesc(moduleDesc: bilibili.app.dynamic.v2.ModuleDesc) =\n                DynamicWordModule(\n                    text = moduleDesc.text\n                )\n        }\n    }\n\n    data class DynamicLiveRcmdModule(\n        val title: String,\n        val cover: String,\n        val roomId: Int\n    ) {\n        companion object {\n            private val json = Json {\n                coerceInputValues = true\n                ignoreUnknownKeys = true\n                prettyPrint = true\n            }\n\n            fun fromModuleDynamic(moduleDynamic: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Dynamic): DynamicLiveRcmdModule {\n                val liveRcmdContent =\n                    json.decodeFromString<LiveRcmdContent>(moduleDynamic.major!!.liveRcmd!!.content)\n                return DynamicLiveRcmdModule(\n                    title = liveRcmdContent.livePlayInfo.title,\n                    cover = liveRcmdContent.livePlayInfo.cover,\n                    roomId = liveRcmdContent.livePlayInfo.roomId\n                )\n            }\n\n            fun fromModuleDynamic(moduleDynamic: bilibili.app.dynamic.v2.ModuleDynamic): DynamicLiveRcmdModule {\n                val liveRcmdContent =\n                    json.decodeFromString<LiveRcmdContent>(moduleDynamic.dynLiveRcmd.content)\n                return DynamicLiveRcmdModule(\n                    title = liveRcmdContent.livePlayInfo.title,\n                    cover = liveRcmdContent.livePlayInfo.cover,\n                    roomId = liveRcmdContent.livePlayInfo.roomId\n                )\n            }\n        }\n\n        @Serializable\n        private data class LiveRcmdContent(\n            @SerialName(\"live_play_info\")\n            val livePlayInfo: LivePlayInfo,\n            @SerialName(\"live_record_info\")\n            val liveRecordInfo: JsonElement? = null,\n            val type: Int\n        ) {\n            @Serializable\n            data class LivePlayInfo(\n                val title: String,\n                @SerialName(\"parent_area_name\")\n                val parentAreaName: String,\n                val cover: String,\n                val online: Int,\n                @SerialName(\"parent_area_id\")\n                val parentAreaId: Int,\n                @SerialName(\"live_start_time\")\n                val liveStartTime: Long,\n                @SerialName(\"room_id\")\n                val roomId: Int,\n                @SerialName(\"live_status\")\n                val liveStatus: Int,\n                @SerialName(\"room_type\")\n                val roomType: Int,\n                @SerialName(\"play_type\")\n                val playType: Int,\n                val link: String,\n                @SerialName(\"area_id\")\n                val areaId: Int,\n                @SerialName(\"area_name\")\n                val areaName: String,\n                @SerialName(\"watched_show\")\n                val watchedShow: WatchedShow,\n                @SerialName(\"room_paid_type\")\n                val roomPaidType: Int,\n                val uid: Long,\n                @SerialName(\"live_screen_type\")\n                val liveScreenType: Int,\n                @SerialName(\"live_id\")\n                val liveId: Long,\n                val pendants: Pendants\n            ) {\n                @Serializable\n                data class WatchedShow(\n                    val num: Int,\n                    @SerialName(\"text_small\")\n                    val textSmall: String,\n                    @SerialName(\"text_large\")\n                    val textLarge: String,\n                    val icon: String,\n                    @SerialName(\"icon_location\")\n                    val iconLocation: String,\n                    @SerialName(\"icon_web\")\n                    val iconWeb: String,\n                    val switch: Boolean\n                )\n\n                @Serializable\n                data class Pendants(\n                    val list: JsonElement? = null\n                )\n            }\n        }\n    }\n\n    data class DynamicPgcModule(\n        val title: String,\n        val epid: Int,\n        val seasonId: Int,\n        val cover: String,\n        val aid: Long,\n        val cid: Long\n    ) {\n        companion object {\n            fun fromModulePgc(modulePgc: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Dynamic.Major.Pgc) =\n                DynamicPgcModule(\n                    title = modulePgc.title,\n                    epid = modulePgc.epid,\n                    seasonId = modulePgc.seasonId,\n                    cover = modulePgc.cover,\n                    aid = 0,\n                    cid = 0\n                )\n\n            fun fromModulePgc(modulePgc: bilibili.app.dynamic.v2.MdlDynPGC): DynamicPgcModule {\n                return DynamicPgcModule(\n                    title = modulePgc.title,\n                    epid = modulePgc.epid.toInt(),\n                    seasonId = modulePgc.seasonId.toInt(),\n                    cover = modulePgc.cover,\n                    aid = modulePgc.aid,\n                    cid = modulePgc.cid\n                )\n            }\n        }\n    }\n\n    data class DynamicArticleModule(\n        val title: String,\n        val text: String,\n        val url: String,\n        val label: String,\n        val id: Int,\n        val covers: List<String>\n    ) {\n        companion object {\n            fun fromModuleArticle(moduleDynamic: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Dynamic.Major.Article) =\n                DynamicArticleModule(\n                    title = moduleDynamic.title,\n                    text = moduleDynamic.desc,\n                    url = moduleDynamic.jumpUrl,\n                    label = moduleDynamic.label,\n                    id = moduleDynamic.id,\n                    covers = moduleDynamic.covers\n                )\n\n            fun fromModuleArticle(moduleArticle: bilibili.app.dynamic.v2.MdlDynArticle): DynamicArticleModule {\n                return DynamicArticleModule(\n                    title = moduleArticle.title,\n                    text = moduleArticle.desc,\n                    url = moduleArticle.uri,\n                    label = moduleArticle.label,\n                    covers = moduleArticle.coversList,\n                    id = moduleArticle.id.toInt()\n                )\n            }\n        }\n    }\n\n    data class DynamicNoneModule(\n        val text: String\n    ) {\n        companion object {\n            fun fromModuleDynamic(moduleNone: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Dynamic.Major.None) =\n                DynamicNoneModule(\n                    text = moduleNone.tips\n                )\n\n            fun fromModuleDynamic(moduleNone: bilibili.app.dynamic.v2.ModuleItemNull): DynamicNoneModule {\n                return DynamicNoneModule(\n                    text = moduleNone.text\n                )\n            }\n        }\n    }\n\n    data class DynamicUgcSeasonModule(\n        val aid: Long,\n        val bvid: String,\n        val cover: String,\n        val desc: String,\n        val duration: String,\n        val url: String,\n        val play: String,\n        val danmaku: String,\n        val title: String\n    ) {\n        companion object {\n            fun fromModuleUgcSeason(moduleDynamic: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem.Modules.Dynamic.Major.UgcSeason) =\n                DynamicUgcSeasonModule(\n                    aid = moduleDynamic.aid,\n                    bvid = moduleDynamic.bvid,\n                    cover = moduleDynamic.cover,\n                    desc = moduleDynamic.desc ?: \"empty description\",\n                    duration = moduleDynamic.durationText,\n                    url = moduleDynamic.jumpUrl,\n                    play = moduleDynamic.stat.play,\n                    danmaku = moduleDynamic.stat.danmaku,\n                    title = moduleDynamic.title\n                )\n        }\n    }\n}\n\ndata class DynamicVideoData(\n    val videos: List<DynamicVideo>,\n    val hasMore: Boolean,\n    val historyOffset: String,\n    val updateBaseline: String\n) {\n    companion object {\n        private val logger = KotlinLogging.logger { }\n        fun fromDynamicData(data: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicData) =\n            DynamicVideoData(\n                videos = data.items.map { DynamicVideo.fromDynamicVideoItem(it) },\n                hasMore = data.hasMore,\n                historyOffset = data.offset,\n                updateBaseline = data.updateBaseline\n            ).also {\n                logger.info { \"updateBaseline: ${data.updateBaseline}\" }\n                logger.info { \"offset: ${data.offset}\" }\n            }\n\n        fun fromDynamicData(data: bilibili.app.dynamic.v2.DynVideoReply) = DynamicVideoData(\n            videos = data.dynamicList.listList.mapNotNull { DynamicVideo.fromDynamicVideoItem(it) },\n            hasMore = data.dynamicList.hasMore,\n            historyOffset = data.dynamicList.historyOffset,\n            updateBaseline = data.dynamicList.updateBaseline\n        ).also {\n            logger.info { \"updateBaseline: ${data.dynamicList.updateBaseline}\" }\n            logger.info { \"historyOffset: ${data.dynamicList.historyOffset}\" }\n        }\n    }\n}\n\n/**\n * 动态视频\n *\n * @property aid 视频av号\n * @property bvid 视频bv号，grpc pgc 没有bv号\n * @property cid 视频cid，仅 grpc 接口\n * @property epid 番剧epid，仅 grpc 接口\n * @property seasonId 番剧seasonId，仅 grpc 接口\n * @property title 视频标题\n * @property cover 视频封面\n * @property author 视频作者\n * @property duration 视频时长，单位秒\n * @property play 视频播放量\n * @property danmaku 视频弹幕数\n * @property avatar 视频作者头像\n * @property pubTime 发布时间\n */\ndata class DynamicVideo(\n    val aid: Long,\n    val bvid: String? = null,\n    val cid: Long,\n    val epid: Int? = null,\n    val seasonId: Int? = null,\n    val title: String,\n    val cover: String,\n    val author: String,\n    var authorId: Long = 0,\n    var authorFace: String = \"\",\n    val duration: Int,\n    val play: Long,\n    val danmaku: Int,\n    val avatar: String,\n    val time: Long = 0L,\n    val pubTime: String? = null,\n    val isChargingArc: Boolean = false,\n    val chargingArcBadge: String = \"\"\n) {\n    companion object {\n        fun fromDynamicVideoItem(item: dev.aaa1115910.biliapi.http.entity.dynamic.DynamicItem): DynamicVideo {\n            val archive = item.modules.moduleDynamic.major!!.archive!!\n            val author = item.modules.moduleAuthor\n            val isChargingArc = archive.badge.text.contains(\"充电\") || archive.badge.text.contains(\"限时免费\")\n            return DynamicVideo(\n                aid = archive.aid.toLong(),\n                bvid = archive.bvid,\n                cid = 0,\n                title = archive.title\n                    .replace(\"动态视频｜\", \"\"),\n                cover = archive.cover,\n                author = author.name,\n                authorId = author.mid,\n                authorFace = author.face,\n                duration = convertStringTimeToSeconds(archive.durationText),\n                play = convertStringPlayCountToNumberPlayCount(archive.stat.play),\n                danmaku = convertStringPlayCountToNumberPlayCount(archive.stat.danmaku).toInt(),\n                avatar = author.face,\n                pubTime = author.pubTime,\n                isChargingArc = isChargingArc,\n                chargingArcBadge = if (isChargingArc) archive.badge.text else \"\"\n            )\n        }\n\n        fun fromDynamicVideoItem(item: bilibili.app.dynamic.v2.DynamicItem): DynamicVideo? {\n            val author =\n                item.modulesList.first { it.moduleType == DynModuleType.module_author }.moduleAuthor.author\n            val dynamic =\n                item.modulesList.first { it.moduleType == DynModuleType.module_dynamic }.moduleDynamic\n            val desc =\n                item.modulesList.firstOrNull { it.moduleType == DynModuleType.module_desc }?.moduleDesc\n            val isDynamicVideo = dynamic.dynArchive?.stype == VideoType.video_type_dynamic\n            when (dynamic.moduleItemCase) {\n                ModuleItemCase.DYN_ARCHIVE -> {\n                    val archive = dynamic.dynArchive\n                    return DynamicVideo(\n                        aid = archive.avid,\n                        bvid = archive.bvid,\n                        cid = archive.cid,\n                        title = if (!isDynamicVideo) archive.title else {\n                            desc?.text?.replace(\"动态视频｜\", \"\") ?: \"NO TITLE\"\n                        },\n                        cover = archive.cover,\n                        author = author.name,\n                        authorId = author.mid,\n                        authorFace = author.face,\n                        duration = convertStringTimeToSeconds(archive.coverLeftText1),\n                        play = convertStringPlayCountToNumberPlayCount(archive.coverLeftText2),\n                        danmaku = convertStringPlayCountToNumberPlayCount(archive.coverLeftText3).toInt(),\n                        avatar = author.face\n                    )\n                }\n\n                ModuleItemCase.DYN_PGC -> {\n                    val pgc = dynamic.dynPgc\n                    return DynamicVideo(\n                        aid = pgc.aid,\n                        bvid = null,\n                        cid = pgc.cid,\n                        epid = pgc.epid.toInt(),\n                        seasonId = pgc.seasonId.toInt(),\n                        title = pgc.title,\n                        cover = pgc.cover,\n                        author = author.name,\n                        duration = convertStringTimeToSeconds(pgc.coverLeftText1),\n                        play = convertStringPlayCountToNumberPlayCount(pgc.coverLeftText2),\n                        danmaku = convertStringPlayCountToNumberPlayCount(pgc.coverLeftText3).toInt(),\n                        avatar = author.face\n                    )\n                }\n\n                ModuleItemCase.DYN_CHARGING_ARCHIVE -> {\n                    val chargingArchiveInfo = dynamic.dynChargingArchive.archiveInfo\n                    return DynamicVideo(\n                        aid = chargingArchiveInfo.avid,\n                        bvid = chargingArchiveInfo.bvid,\n                        cid = chargingArchiveInfo.cid,\n                        title = chargingArchiveInfo.title,\n                        cover = chargingArchiveInfo.cover,\n                        author = author.name,\n                        duration = convertStringTimeToSeconds(chargingArchiveInfo.coverLeftText1),\n                        play = convertStringPlayCountToNumberPlayCount(chargingArchiveInfo.coverLeftText2),\n                        danmaku = convertStringPlayCountToNumberPlayCount(chargingArchiveInfo.coverLeftText3).toInt(),\n                        avatar = author.face\n                    )\n                }\n\n                else -> {\n                    println(\"unsupported dynamic moduleItemCase: ${dynamic.moduleItemCase}\")\n                    return null\n                }\n            }\n        }\n    }\n}\n\nprivate fun convertStringTimeToSeconds(time: String): Int {\n    //部分稿件可能没有时长，Web 接口返回 NaN:NaN:NaN，App 接口返回空字符串\n    if (time.startsWith(\"NaN\") || time.isBlank()) return 0\n\n    val parts = time.split(\":\")\n    val hours = if (parts.size == 3) parts[0].toInt() else 0\n    val minutes = parts[parts.size - 2].toInt()\n    val seconds = parts[parts.size - 1].toInt()\n    return (hours * 3600) + (minutes * 60) + seconds\n}\n\n//web 接口获取到的是“xx万”，而 grpc 接口获取到的是“xx.x万播放”\nprivate fun convertStringPlayCountToNumberPlayCount(play: String): Long {\n    if (play.startsWith(\"-\")) return 0\n    runCatching {\n        val number = play\n            .replace(\"弹幕\", \"\")\n            .replace(\"观看\", \"\")\n            .replace(\"播放\", \"\")\n            .substringBefore(\"万\").toFloat()\n        return (if (play.contains(\"万\")) number * 10000 else number).toLong()\n    }.onFailure {\n        println(\"convert play count [$play] failed: ${it.stackTraceToString()}\")\n    }\n    return -1\n}\n\nenum class DynamicType(val webValue: String, val appValue: bilibili.app.dynamic.v2.DynamicType) {\n    Av(\"DYNAMIC_TYPE_AV\", bilibili.app.dynamic.v2.DynamicType.av),\n\n    // bilibili hd 端的接口并不会返回合集更新动态\n    UgcSeason(\"DYNAMIC_TYPE_UGC_SEASON\", bilibili.app.dynamic.v2.DynamicType.ugc_season),\n    Forward(\"DYNAMIC_TYPE_FORWARD\", bilibili.app.dynamic.v2.DynamicType.forward),\n    Word(\"DYNAMIC_TYPE_WORD\", bilibili.app.dynamic.v2.DynamicType.word),\n    Draw(\"DYNAMIC_TYPE_DRAW\", bilibili.app.dynamic.v2.DynamicType.draw),\n    LiveRcmd(\"DYNAMIC_TYPE_LIVE_RCMD\", bilibili.app.dynamic.v2.DynamicType.live_rcmd),\n    Pgc(\"DYNAMIC_TYPE_PGC_UNION\", bilibili.app.dynamic.v2.DynamicType.pgc),\n    Article(\"DYNAMIC_TYPE_ARTICLE\", bilibili.app.dynamic.v2.DynamicType.article),\n    None(\"DYNAMIC_TYPE_NONE\", bilibili.app.dynamic.v2.DynamicType.dyn_none);\n\n    companion object {\n        fun fromWebValue(webValue: String) = entries.firstOrNull { it.webValue == webValue }\n            ?: throw IllegalArgumentException(\"unknown type $webValue\")\n\n        fun fromAppValue(appValue: bilibili.app.dynamic.v2.DynamicType) =\n            entries.firstOrNull { it.appValue == appValue }\n                ?: throw IllegalArgumentException(\"unknown type ${appValue.name}\")\n    }\n}\n\nprivate fun Module.isAuthorModule() = moduleType == DynModuleType.module_author\nprivate fun Module.isAuthorModuleForward() = moduleType == DynModuleType.module_author_forward\nprivate fun Module.isDescModule() = moduleType == DynModuleType.module_desc\nprivate fun Module.isDynamicModule() = moduleType == DynModuleType.module_dynamic\nprivate fun Module.isModuleOpusSummary() = moduleType == DynModuleType.module_opus_summary\nprivate fun Module.isStatModule() = moduleType == DynModuleType.module_stat\nprivate fun Module.isBottomModel() = moduleType == DynModuleType.module_bottom\nprivate fun Module.isItemNullModel() = moduleType == DynModuleType.module_item_null\nprivate fun Module.isParagraphModel() = moduleType == DynModuleType.module_paragraph\n\nprivate fun bilibili.app.dynamic.v2.DynamicItem.getAuthorModule() =\n    modulesList.firstOrNull { it.isAuthorModule() }?.moduleAuthor\n\nprivate fun bilibili.app.dynamic.v2.DynamicItem.getAuthorModuleForward() =\n    modulesList.firstOrNull { it.isAuthorModuleForward() }?.moduleAuthorForward\n\nprivate fun bilibili.app.dynamic.v2.DynamicItem.getDescModule() =\n    modulesList.firstOrNull { it.isDescModule() }?.moduleDesc\n\nprivate fun bilibili.app.dynamic.v2.DynamicItem.getDynamicModule() =\n    modulesList.firstOrNull { it.isDynamicModule() }?.moduleDynamic\n\nprivate fun bilibili.app.dynamic.v2.DynamicItem.getOpusSummaryModule() =\n    modulesList.firstOrNull { it.isModuleOpusSummary() }?.moduleOpusSummary\n\nprivate fun bilibili.app.dynamic.v2.DynamicItem.getStatModule() =\n    modulesList.firstOrNull { it.isStatModule() }?.moduleStat\n\nprivate fun bilibili.app.dynamic.v2.DynamicItem.getBottomModule() =\n    modulesList.firstOrNull { it.isBottomModel() }?.moduleButtom\n\nprivate fun bilibili.app.dynamic.v2.DynamicItem.getItemNullModule() =\n    modulesList.firstOrNull { it.isItemNullModel() }?.moduleItemNull\n\nprivate fun bilibili.app.dynamic.v2.DynamicItem.getParagraphModule() =\n    modulesList.firstOrNull { it.isParagraphModel() }?.moduleParagraph"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/user/FollowedUser.kt",
    "content": "package dev.aaa1115910.biliapi.entity.user\n\ndata class FollowedUser(\n    val mid: Long,\n    val name: String,\n    val avatar: String,\n    val sign: String\n) {\n    companion object {\n        fun fromHttpFollowedUser(followedUser: dev.aaa1115910.biliapi.http.entity.user.UserFollowData.FollowedUser) =\n            FollowedUser(\n                mid = followedUser.mid,\n                name = followedUser.uname,\n                avatar = followedUser.face,\n                sign = followedUser.sign\n            )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/user/History.kt",
    "content": "package dev.aaa1115910.biliapi.entity.user\n\nimport bilibili.app.interfaces.v1.CursorItem\n\n//TODO 暂时仅解析 UGC 和 PGC\ndata class HistoryData(\n    val cursor: Long,\n    val data: List<HistoryItem>\n) {\n    companion object {\n        fun fromHistoryResponse(data: dev.aaa1115910.biliapi.http.entity.history.HistoryData) =\n            HistoryData(\n                cursor = data.cursor.viewAt,\n                data = data.list\n                    .filter { it.history.business == \"archive\" || it.history.business == \"pgc\" }\n                    .map { HistoryItem.fromHistoryItem(it) }\n            )\n\n        fun fromHistoryResponse(data: bilibili.app.interfaces.v1.CursorV2Reply) = HistoryData(\n            cursor = data.cursor.max,\n            data = data.itemsList\n                .filter { it.cardItemCase == CursorItem.CardItemCase.CARD_UGC || it.cardItemCase == CursorItem.CardItemCase.CARD_OGV }\n                .map { HistoryItem.fromHistoryItem(it) }\n        )\n    }\n}\n\ndata class HistoryItem(\n    val oid: Long,\n    val bvid: String,\n    val cid: Long,\n    val kid: Long,\n    val epid: Int?,\n    val seasonId: Int?,\n    val title: String,\n    val cover: String,\n    val author: String,\n    val authorId: Long = 0,\n    val authorFace: String = \"\",\n    val duration: Int,\n    val progress: Int,\n    val type: HistoryItemType,\n    val viewAt: Long\n) {\n    companion object {\n        fun fromHistoryItem(item: dev.aaa1115910.biliapi.http.entity.history.HistoryItem) =\n            HistoryItem(\n                oid = item.history.oid,\n                bvid = item.history.bvid,\n                cid = item.history.cid,\n                kid = item.kid,\n                epid = item.history.epid,\n                seasonId = null,\n                title = when(item.history.business){\n                    \"archive\" -> item.title\n                    \"pgc\" -> item.title + \"\\n\" + item.showTitle\n                    else -> item.title\n                },\n                cover = item.cover,\n                author = item.authorName,\n                authorId = item.authorMid,\n                authorFace = item.authorFace,\n                duration = item.duration,\n                progress = item.progress,\n                type = when (item.history.business) {\n                    \"archive\" -> HistoryItemType.Archive\n                    \"pgc\" -> HistoryItemType.Pgc\n                    else -> HistoryItemType.Unknown\n                },\n                viewAt = item.viewAt\n            )\n\n        @Suppress(\"RemoveRedundantQualifierName\")\n        fun fromHistoryItem(item: bilibili.app.interfaces.v1.CursorItem) = HistoryItem(\n            oid = item.oid,\n            bvid = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.bvid\n                CursorItem.CardItemCase.CARD_OGV -> \"\"\n                else -> \"\"\n            },\n            cid = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.cid\n                CursorItem.CardItemCase.CARD_OGV -> 0\n                else -> 0\n            },\n            kid = item.kid,\n            epid = null,\n            seasonId = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_OGV -> item.kid.toInt()\n                else -> null\n            },\n            title = item.title,\n            cover = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.cover\n                CursorItem.CardItemCase.CARD_OGV -> item.cardOgv.cover\n                else -> \"\"\n            },\n            author = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.name\n                CursorItem.CardItemCase.CARD_OGV -> \"\"\n                else -> \"\"\n            },\n            authorId =  when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.mid\n                CursorItem.CardItemCase.CARD_OGV -> 0\n                else -> 0\n            },\n            duration = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.duration.toInt()\n                CursorItem.CardItemCase.CARD_OGV -> item.cardOgv.duration.toInt()\n                else -> 0\n            },\n            progress = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.progress.toInt()\n                CursorItem.CardItemCase.CARD_OGV -> item.cardOgv.progress.toInt()\n                else -> 0\n            },\n            type = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> HistoryItemType.Archive\n                CursorItem.CardItemCase.CARD_OGV -> HistoryItemType.Pgc\n                else -> HistoryItemType.Unknown\n            },\n            viewAt = item.viewAt\n        )\n    }\n}\n\nenum class HistoryItemType {\n    Unknown, Archive, Pgc\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/user/Space.kt",
    "content": "package dev.aaa1115910.biliapi.entity.user\n\nimport java.util.Date\n\ndata class SpaceVideoData(\n    val videos: List<SpaceVideo>,\n    val page: SpaceVideoPage\n) {\n    companion object {\n        fun fromWebSpaceVideoData(webSpaceVideoData: dev.aaa1115910.biliapi.http.entity.user.WebSpaceVideoData) =\n            SpaceVideoData(\n                videos = webSpaceVideoData.list?.vlist\n                    ?.map { SpaceVideo.fromSpaceVideoItem(it) }\n                    ?: emptyList(),\n                page = SpaceVideoPage(\n                    hasNext = (webSpaceVideoData.page?.count ?: 0)\n                            > ((webSpaceVideoData.page?.pageNumber ?: 0)\n                            * (webSpaceVideoData.page?.pageSize ?: 0)),\n                    nextWebPageSize = webSpaceVideoData.page?.pageSize ?: 0,\n                    nextWebPageNumber = (webSpaceVideoData.page?.pageNumber ?: 0) + 1\n                )\n            )\n\n        fun fromAppSpaceVideoData(appSpaceVideoData: dev.aaa1115910.biliapi.http.entity.user.AppSpaceVideoData) =\n            SpaceVideoData(\n                videos = appSpaceVideoData.item\n                    .map { SpaceVideo.fromSpaceVideoItem(it) },\n                page = SpaceVideoPage(\n                    hasNext = appSpaceVideoData.hasNext,\n                    lastAvid = appSpaceVideoData.item.lastOrNull()?.param?.toLong() ?: 0\n                )\n            )\n    }\n}\n\ndata class SpaceVideo(\n    val aid: Long,\n    val bvid: String,\n    val title: String,\n    val cover: String,\n    val author: String,\n    val authorId: Long = 0,\n    val duration: Int,\n    val play: Long,\n    val danmaku: Int,\n    val publishDate: Date,\n    val isChargingArc: Boolean = false,\n    val chargingArcBadge: String = \"\"\n) {\n    companion object {\n        fun fromSpaceVideoItem(spaceVideoItem: dev.aaa1115910.biliapi.http.entity.user.WebSpaceVideoData.SpaceVideoListItem.VListItem) =\n            SpaceVideo(\n                aid = spaceVideoItem.aid,\n                bvid = spaceVideoItem.bvid,\n                title = spaceVideoItem.title,\n                cover = spaceVideoItem.pic,\n                author = spaceVideoItem.author,\n                authorId = spaceVideoItem.mid,\n                duration = convertMmSsToSeconds(spaceVideoItem.length),\n                play = spaceVideoItem.play,\n                danmaku = spaceVideoItem.videoReview,\n                publishDate = Date(spaceVideoItem.created * 1000L),\n                isChargingArc = spaceVideoItem.isChargingArc,\n                chargingArcBadge = spaceVideoItem.elecArcBadge\n            )\n\n        fun fromSpaceVideoItem(spaceVideoItem: dev.aaa1115910.biliapi.http.entity.user.AppSpaceVideoData.SpaceVideoItem) =\n            SpaceVideo(\n                aid = spaceVideoItem.param.toLong(),\n                bvid = spaceVideoItem.bvid ?: \"\",\n                title = spaceVideoItem.title,\n                cover = spaceVideoItem.cover,\n                author = spaceVideoItem.author ?: \"\",\n                duration = spaceVideoItem.duration,\n                play = spaceVideoItem.play,\n                danmaku = spaceVideoItem.danmaku,\n                publishDate = Date(spaceVideoItem.ctime * 1000L)\n            )\n    }\n}\n\nprivate fun convertMmSsToSeconds(time: String): Int {\n    val parts = time.split(\":\")\n    val minutes = parts[0].toInt()\n    val seconds = parts[1].toInt()\n    return (minutes * 60) + seconds\n}\n\nenum class SpaceVideoOrder(val value: String) {\n    PubDate(\"pubdate\"), Click(\"click\")\n}\n\ndata class SpaceVideoPage(\n    val hasNext: Boolean = true,\n    // web\n    val nextWebPageSize: Int = 20,\n    val nextWebPageNumber: Int = 1,\n    // app\n    val lastAvid: Long = 0\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/user/ToView.kt",
    "content": "package dev.aaa1115910.biliapi.entity.user\n\nimport bilibili.app.interfaces.v1.CursorItem\n\n//TODO 暂时仅解析 UGC 和 PGC\ndata class ToViewData(\n    val cursor: Long,\n    val data: List<ToViewItem>\n) {\n    companion object {\n        fun fromToViewResponse(data: dev.aaa1115910.biliapi.http.entity.toview.ToViewData) =\n            ToViewData(\n//               cursor = data.cursor.viewAt,\n                cursor = 0,\n                data = data.list\n//                    .filter { it.history.business == \"archive\" || it.history.business == \"pgc\" }\n                    .map { ToViewItem.fromToViewItem(it) }\n            )\n\n        // fun fromToViewResponse(data: bilibili.app.interfaces.v1.CursorV2Reply) = ToViewData(\n        //     cursor = data.cursor.max,\n        //     data = data.itemsList\n        //         .filter { it.cardItemCase == CursorItem.CardItemCase.CARD_UGC || it.cardItemCase == CursorItem.CardItemCase.CARD_OGV }\n        //         .map { ToViewItem.fromToViewItem(it) }\n        // )\n    }\n}\n\ndata class ToViewItem(\n    val oid: Long,\n    val bvid: String,\n    val cid: Long,\n    val kid: Int,\n    val epid: Int?,\n    val seasonId: Int?,\n    val title: String,\n    val cover: String,\n    val author: String,\n    val authorId: Long,\n    val authorFace: String = \"\",\n    val duration: Int,\n    val progress: Int,\n    val type: ToViewItemType,\n    val pubdate: Long,\n    val play: Long,\n    val danmaku: Int\n) {\n    companion object {\n        fun fromToViewItem(item: dev.aaa1115910.biliapi.http.entity.toview.ToViewItem) =\n            ToViewItem(\n                oid = item.aid,\n                bvid = item.bvid,\n                cid = item.cid,\n                kid = 0,\n                epid = 0,\n                seasonId = null,\n                title = item.title,\n                cover = item.pic,\n                author = item.owner.name,\n                authorId = item.owner.mid,\n                authorFace = item.owner.face,\n                duration = item.duration,\n                progress = item.progress,\n                type = ToViewItemType.Archive,\n                pubdate = item.pubdate,\n                play = item.stat.view,\n                danmaku = item.stat.danmaku\n                // type = when (item.history.business) {\n                //     \"archive\" -> HistoryItemType.Archive\n                //     \"pgc\" -> HistoryItemType.Pgc\n                //     else -> HistoryItemType.Unknown\n                // }\n            )\n\n        @Suppress(\"RemoveRedundantQualifierName\")\n        fun fromToViewItem(item: bilibili.app.interfaces.v1.CursorItem) = ToViewItem(\n            oid = item.oid,\n            bvid = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.bvid\n                CursorItem.CardItemCase.CARD_OGV -> \"\"\n                else -> \"\"\n            },\n            cid = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.cid\n                CursorItem.CardItemCase.CARD_OGV -> 0\n                else -> 0\n            },\n            kid = item.kid.toInt(),\n            epid = null,\n            seasonId = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_OGV -> item.kid.toInt()\n                else -> null\n            },\n            title = item.title,\n            cover = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.cover\n                CursorItem.CardItemCase.CARD_OGV -> item.cardOgv.cover\n                else -> \"\"\n            },\n            author = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.name\n                CursorItem.CardItemCase.CARD_OGV -> \"\"\n                else -> \"\"\n            },\n            authorId = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.mid\n                CursorItem.CardItemCase.CARD_OGV -> 0\n                else -> 0\n            },\n            duration = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.duration.toInt()\n                CursorItem.CardItemCase.CARD_OGV -> item.cardOgv.duration.toInt()\n                else -> 0\n            },\n            progress = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> item.cardUgc.progress.toInt()\n                CursorItem.CardItemCase.CARD_OGV -> item.cardOgv.progress.toInt()\n                else -> 0\n            },\n            type = when (item.cardItemCase) {\n                CursorItem.CardItemCase.CARD_UGC -> ToViewItemType.Archive\n                CursorItem.CardItemCase.CARD_OGV -> ToViewItemType.Pgc\n                else -> ToViewItemType.Unknown\n            },\n            pubdate = -1,\n            play = -1,\n            danmaku = -1\n        )\n    }\n}\n\nenum class ToViewItemType {\n    Unknown, Archive, Pgc\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/Dimension.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video\n\ndata class Dimension(\n    val width: Int,\n    val height: Int,\n    val isVertical: Boolean = width < height\n) {\n    companion object {\n        fun fromDimension(dimension: bilibili.app.archive.v1.Dimension) = Dimension(\n            width = dimension.width.toInt(),\n            height = dimension.height.toInt()\n        )\n\n        fun fromDimension(dimension: dev.aaa1115910.biliapi.http.entity.video.Dimension) =\n            Dimension(\n                width = dimension.width,\n                height = dimension.height\n            )\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/Heartbeat.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video\n\nenum class HeartbeatVideoType(val value: Int) {\n    Video(3), Season(4), Course(10)\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/RelatedVideo.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video\n\nimport bilibili.app.view.v1.authorOrNull\nimport dev.aaa1115910.biliapi.entity.ugc.toSmartDate\nimport dev.aaa1115910.biliapi.entity.user.Author\n\ndata class RelatedVideo(\n    val aid: Long,\n    val cover: String,\n    val title: String,\n    val duration: Int,\n    val author: Author?,\n    val jumpToSeason: Boolean,\n    val epid: Int?,\n    val view: Long,\n    val danmaku: Int,\n    val pubTime: String? = null,\n    val isChargingArchive: Boolean = false\n) {\n    companion object {\n        fun fromRelate(relate: bilibili.app.view.v1.Relate) = RelatedVideo(\n            aid = relate.aid,\n            cover = relate.pic,\n            title = relate.title,\n            duration = relate.duration.toInt(),\n            author = relate.authorOrNull?.let { Author.fromAuthor(it) }\n                ?: relate.desc?.let { Author(0, it, \"\") },\n            jumpToSeason = relate.goto.needJumpToSeason(),\n            epid = if (relate.goto.needJumpToSeason()) relate.uri.substringBeforeLast(\"?\")\n                .substringAfterLast(\"/ep\").toInt() else null,\n            view = relate.stat.view,\n            danmaku = relate.stat.danmaku\n        )\n\n        fun fromRelate(relate: dev.aaa1115910.biliapi.http.entity.video.RelatedVideoInfo) =\n            RelatedVideo(\n                aid = relate.aid,\n                cover = relate.pic,\n                title = relate.title,\n                duration = relate.duration,\n                author = relate.owner.let { Author.fromVideoOwner(it) },\n                jumpToSeason = false,\n                epid = null,\n                view = relate.stat.view,\n                danmaku = relate.stat.danmaku,\n                pubTime = relate.pubdate.toLong().toSmartDate(),\n                isChargingArchive = relate.isChargingArchive || relate.chargingPay != null\n            )\n    }\n}\n\nprivate fun String.needJumpToSeason() = this.contains(\"bangumi_ep\") || this.contains(\"special\")\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/Subtitle.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video\n\n//TODO 将 lanDoc 内括号的内容分离出来，将其作为 Badge 显示\n/**\n * 字幕\n *\n * @param id 字幕 id\n * @param lang 字幕语言，例如 zh-Hant ai-zh ai-en\n * @param langDoc 字幕语言名称\n * @param url 字幕地址\n * @param type 字幕类型 人工/AI\n * @param aiType AI 字幕类型 生成/翻译\n * @param aiStatus AI 字幕状态\n */\ndata class Subtitle(\n    val id: Long,\n    val lang: String,\n    val langDoc: String,\n    val url: String,\n    var type: SubtitleType,\n    val aiType: SubtitleAiType,\n    var aiStatus: SubtitleAiStatus\n) {\n    companion object {\n        fun fromSubtitleItem(data: dev.aaa1115910.biliapi.http.entity.video.VideoMoreInfo.SubtitleItem) =\n            Subtitle(\n                id = data.id,\n                lang = data.lan,\n                langDoc = data.lanDoc + if (data.type == SubtitleType.AI.ordinal) \"(AI)\" else \"\",\n                url = data.subtitleUrl,\n                type = when (data.type) {\n                    0 -> SubtitleType.CC\n                    1 -> SubtitleType.AI\n                    else -> SubtitleType.CC\n                },\n                aiType = when (data.aiType) {\n                    0 -> SubtitleAiType.Normal\n                    1 -> SubtitleAiType.Translate\n                    else -> SubtitleAiType.Normal\n                },\n                aiStatus = when (data.aiStatus) {\n                    0 -> SubtitleAiStatus.None\n                    1 -> SubtitleAiStatus.Exposure\n                    2 -> SubtitleAiStatus.Assist\n                    else -> SubtitleAiStatus.None\n                }\n            )\n\n        fun fromSubtitleItem(data: bilibili.community.service.dm.v1.SubtitleItem) =\n            Subtitle(\n                id = data.id,\n                lang = data.lan,\n                langDoc = data.lanDoc,\n                url = data.subtitleUrl,\n                type = when (data.type) {\n                    bilibili.community.service.dm.v1.SubtitleType.CC -> SubtitleType.CC\n                    bilibili.community.service.dm.v1.SubtitleType.AI -> SubtitleType.AI\n                    else -> SubtitleType.CC\n                },\n                aiType = when (data.aiType) {\n                    bilibili.community.service.dm.v1.SubtitleAiType.Normal -> SubtitleAiType.Normal\n                    bilibili.community.service.dm.v1.SubtitleAiType.Translate -> SubtitleAiType.Translate\n                    else -> SubtitleAiType.Normal\n                },\n                aiStatus = when (data.aiStatus) {\n                    bilibili.community.service.dm.v1.SubtitleAiStatus.None -> SubtitleAiStatus.None\n                    bilibili.community.service.dm.v1.SubtitleAiStatus.Exposure -> SubtitleAiStatus.Exposure\n                    bilibili.community.service.dm.v1.SubtitleAiStatus.Assist -> SubtitleAiStatus.Assist\n                    else -> SubtitleAiStatus.None\n                }\n            )\n    }\n}\n\nenum class SubtitleType {\n    CC, AI\n}\n\nenum class SubtitleAiType {\n    Normal, Translate\n}\n\nenum class SubtitleAiStatus {\n    None, Exposure, Assist\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/Tag.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video\n\ndata class Tag(\n    val id: Int,\n    val name: String\n) {\n    companion object {\n        fun fromTag(tag: dev.aaa1115910.biliapi.http.entity.video.Tag) = Tag(\n            id = tag.tagId,\n            name = tag.tagName\n        )\n\n        fun fromTag(tag: bilibili.app.view.v1.Tag) = Tag(\n            id = tag.id.toInt(),\n            name = tag.name\n        )\n\n        fun fromTag(tag: dev.aaa1115910.biliapi.http.entity.video.VideoDetail.Tag) = Tag(\n            id = tag.tagId,\n            name = tag.tagName\n        )\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/VideoDetail.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video\n\nimport bilibili.app.view.v1.ReqUser\nimport bilibili.app.view.v1.ViewReply\nimport bilibili.app.view.v1.ugcSeasonOrNull\nimport dev.aaa1115910.biliapi.entity.user.Author\nimport dev.aaa1115910.biliapi.entity.video.season.UgcSeason\nimport dev.aaa1115910.biliapi.http.entity.video.VideoStat\nimport java.util.Date\n\ndata class VideoDetail(\n    val bvid: String,\n    val aid: Long,\n    val cid: Long,\n    val cover: String,\n    val title: String,\n    val publishDate: Date,\n    val description: String,\n    val stat: Stat,\n    val author: Author,\n    val pages: List<VideoPage>,\n    val ugcSeason: UgcSeason?,\n    val relatedVideos: List<RelatedVideo>,\n    val redirectToEp: Boolean,\n    val epid: Int?,\n    val argueTip: String?,\n    val tags: List<Tag>,\n    val userActions: UserActions,\n    var history: History,\n    var playerIcon: PlayerIcon? = null,\n    var isChargingArc: Boolean = false,\n    var chargingArcBadge: String = \"\"\n) {\n    companion object {\n        fun fromViewReply(viewReply: ViewReply): VideoDetail {\n            if (!viewReply.hasActivitySeason()) {\n                return VideoDetail(\n                    bvid = viewReply.bvid,\n                    aid = viewReply.arc.aid,\n                    cid = viewReply.arc.firstCid,\n                    cover = viewReply.arc.pic,\n                    title = viewReply.arc.title,\n                    publishDate = Date(viewReply.arc.pubdate * 1000L),\n                    description = viewReply.arc.desc,\n                    stat = Stat.fromStat(viewReply.arc.stat),\n                    author = Author.fromAuthor(viewReply.arc.author),\n                    pages = viewReply.pagesList.map { VideoPage.fromViewPage(it) },\n                    ugcSeason = viewReply.ugcSeasonOrNull?.let { UgcSeason.fromUgcSeason(it) },\n                    relatedVideos = viewReply.relatesList.map { RelatedVideo.fromRelate(it) },\n                    redirectToEp = viewReply.arc.redirectUrl.contains(\"ep\"),\n                    epid = runCatching {\n                        viewReply.arc.redirectUrl.split(\"ep\", \"?\")[1].toInt()\n                    }.getOrNull(),\n                    argueTip = viewReply.argueMsg.takeIf { it.isNotEmpty() },\n                    tags = viewReply.tagList.map { Tag.fromTag(it) },\n                    userActions = UserActions.fromReqUser(viewReply.reqUser),\n                    history = History.fromHistory(viewReply.history),\n                    playerIcon = viewReply.playerIcon?.let { PlayerIcon.fromPlayerIcon(it) }\n                )\n            } else {\n                return VideoDetail(\n                    bvid = viewReply.activitySeason.bvid,\n                    aid = viewReply.activitySeason.arc.aid,\n                    cid = viewReply.activitySeason.arc.firstCid,\n                    cover = viewReply.activitySeason.arc.pic,\n                    title = viewReply.activitySeason.arc.title,\n                    publishDate = Date(viewReply.activitySeason.arc.pubdate * 1000L),\n                    description = viewReply.activitySeason.arc.desc,\n                    stat = Stat.fromStat(viewReply.activitySeason.arc.stat),\n                    author = Author.fromAuthor(viewReply.activitySeason.arc.author),\n                    pages = viewReply.activitySeason.pagesList.map { VideoPage.fromViewPage(it) },\n                    ugcSeason = viewReply.activitySeason.ugcSeasonOrNull?.let {\n                        UgcSeason.fromUgcSeason(\n                            it\n                        )\n                    },\n                    relatedVideos = viewReply.relatesList.map { RelatedVideo.fromRelate(it) },\n                    redirectToEp = viewReply.activitySeason.arc.redirectUrl.contains(\"ep\"),\n                    epid = runCatching {\n                        viewReply.activitySeason.arc.redirectUrl.split(\"ep\", \"?\")[1].toInt()\n                    }.getOrNull(),\n                    argueTip = viewReply.activitySeason.argueMsg.takeIf { it.isNotEmpty() },\n                    tags = viewReply.tagList.map { Tag.fromTag(it) },\n                    userActions = UserActions.fromReqUser(viewReply.activitySeason.reqUser),\n                    history = History.fromHistory(viewReply.activitySeason.history),\n                    playerIcon = viewReply.activitySeason.playerIcon?.let {\n                        PlayerIcon.fromPlayerIcon(\n                            it\n                        )\n                    }\n                )\n            }\n        }\n\n        fun fromVideoDetail(videoDetail: dev.aaa1115910.biliapi.http.entity.video.VideoDetail) =\n            VideoDetail(\n                bvid = videoDetail.view.bvid,\n                aid = videoDetail.view.aid,\n                cid = videoDetail.view.cid,\n                cover = videoDetail.view.pic,\n                title = videoDetail.view.title,\n                publishDate = Date(videoDetail.view.pubdate * 1000L),\n                description = videoDetail.view.desc,\n                stat = Stat.fromVideoStat(videoDetail.view.stat),\n                author = Author.fromVideoOwner(videoDetail.view.owner),\n                pages = videoDetail.view.pages.map { VideoPage.fromVideoPage(it) },\n                ugcSeason = videoDetail.view.ugcSeason?.let { UgcSeason.fromUgcSeason(it) },\n                relatedVideos = videoDetail.related?.map { RelatedVideo.fromRelate(it) }\n                    ?: emptyList(),\n                redirectToEp = videoDetail.view.redirectUrl?.contains(\"ep\") ?: false,\n                epid = videoDetail.view.redirectUrl?.split(\"ep\", \"?\")?.get(1)?.toInt(),\n                argueTip = videoDetail.view.stat.argueMsg.takeIf { it.isNotEmpty() },\n                tags = videoDetail.tags.map { Tag.fromTag(it) },\n                userActions = UserActions(),\n                history = History(0, 0),\n                playerIcon = null,\n                isChargingArc = videoDetail.view.isUpowerExclusive,\n                chargingArcBadge = if (videoDetail.view.isUpowerExclusive) {\n                    if (videoDetail.view.isUpowerPlay) \"限时免费\" else \"充电专属\"\n                } else \"\"\n            )\n    }\n\n    data class Stat(\n        val view: Long,\n        val danmaku: Int,\n        val reply: Int,\n        val favorite: Int,\n        val coin: Int,\n        val share: Int,\n        val like: Int,\n        val historyRank: Int\n    ) {\n        companion object {\n            fun fromStat(stat: bilibili.app.archive.v1.Stat) = Stat(\n                view = stat.view,\n                danmaku = stat.danmaku,\n                reply = stat.reply,\n                favorite = stat.fav,\n                coin = stat.coin,\n                share = stat.share,\n                like = stat.like,\n                historyRank = stat.hisRank\n            )\n\n            fun fromVideoStat(videoStat: VideoStat) = Stat(\n                view = videoStat.view,\n                danmaku = videoStat.danmaku,\n                reply = videoStat.reply,\n                favorite = videoStat.favorite,\n                coin = videoStat.coin,\n                share = videoStat.share,\n                like = videoStat.like,\n                historyRank = videoStat.hisRank\n            )\n        }\n    }\n\n    data class History(\n        val progress: Int,\n        val lastPlayedCid: Long\n    ) {\n        companion object {\n            fun fromHistory(history: bilibili.app.view.v1.History) = History(\n                progress = history.progress.toInt(),\n                lastPlayedCid = history.cid\n            )\n        }\n    }\n\n    data class PlayerIcon(\n        val idle: String,\n        val moving: String\n    ) {\n        companion object {\n            fun fromPlayerIcon(playerIcon: dev.aaa1115910.biliapi.http.entity.video.VideoMoreInfo.PlayerIcon?) =\n                if (playerIcon != null && playerIcon.url1 != null && playerIcon.url2 != null)\n                    PlayerIcon(\n                        idle = playerIcon.url2,\n                        moving = playerIcon.url1\n                    )\n                else null\n\n            fun fromPlayerIcon(playerIcon: bilibili.app.view.v1.PlayerIcon) = PlayerIcon(\n                idle = playerIcon.url2,\n                moving = playerIcon.url1\n            )\n\n            fun fromPlayerIcon(playerIcon: dev.aaa1115910.biliapi.http.entity.season.AppSeasonData.PlayerIcon?) =\n                playerIcon?.let {\n                    PlayerIcon(\n                        idle = playerIcon.url2 ?: return@let null,\n                        moving = playerIcon.url1 ?: return@let null\n                    )\n                }\n        }\n    }\n}\n\ndata class UserActions(\n    var like: Boolean = false,\n    var favorite: Boolean = false,\n    var coin: Boolean = false,\n    var dislike: Boolean = false\n) {\n    companion object {\n        fun fromReqUser(reqUser: ReqUser): UserActions {\n            return UserActions(\n                like = reqUser.like == 1,\n                favorite = reqUser.favorite == 1,\n                coin = reqUser.coin == 1,\n                dislike = reqUser.dislike == 1\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/VideoPage.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video\n\ndata class VideoPage(\n    var cid: Long,\n    val index: Int,\n    val title: String,\n    val duration: Int,\n    val dimension: Dimension\n) {\n    companion object {\n        fun fromViewPage(viewPage: bilibili.app.view.v1.ViewPage) = VideoPage(\n            cid = viewPage.page.cid,\n            index = viewPage.page.page,\n            title = viewPage.page.part,\n            duration = viewPage.page.duration.toInt(),\n            dimension = Dimension.fromDimension(viewPage.page.dimension)\n        )\n\n        fun fromVideoPage(videoPage: dev.aaa1115910.biliapi.http.entity.video.VideoPage) =\n            VideoPage(\n                cid = videoPage.cid,\n                index = videoPage.page,\n                title = videoPage.part,\n                duration = videoPage.duration,\n                dimension = Dimension.fromDimension(videoPage.dimension)\n            )\n\n        fun fromPage(page: bilibili.app.archive.v1.Page) = VideoPage(\n            cid = page.cid,\n            index = page.page,\n            title = page.part,\n            duration = page.duration.toInt(),\n            dimension = Dimension.fromDimension(page.dimension)\n        )\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/VideoShot.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video\n\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayInputStream\nimport java.io.DataInputStream\n\ndata class VideoShot(\n    val times: List<UShort>,\n    val images: List<ByteArray?>,\n    val imageCountX: Int,\n    val imageCountY: Int,\n    val imageWidth: Int,\n    val imageHeight: Int\n) {\n    companion object {\n        suspend fun fromVideoShot(videoShot: dev.aaa1115910.biliapi.http.entity.video.VideoShot): VideoShot? =\n            withContext(Dispatchers.IO) {\n                val images = videoShot.image.map { imageUrl ->\n                    async {\n                        runCatching {\n                            BiliHttpApi.download(imageUrl)\n                        }.getOrNull()\n                    }\n                }.awaitAll()\n                if (images.contains(null)) {\n                    println(\"download video shot images failed\")\n                    return@withContext null\n                }\n\n                val timeBinary = runCatching {\n                    BiliHttpApi.download(\n                        videoShot.pvData ?: throw IllegalStateException(\"pvData is null\")\n                    )\n                }.onFailure {\n                    println(\"download video shot times binary failed: ${it.stackTraceToString()}\")\n                    return@withContext null\n                }.getOrNull()\n\n                val times = mutableListOf<UShort>()\n                runCatching {\n                    DataInputStream(ByteArrayInputStream(timeBinary)).use {\n                        //if has next\n                        while (it.available() > 0) {\n                            times.add(it.readUnsignedShort().toUShort())\n                        }\n                    }\n                }.onFailure {\n                    println(\"parse video shot times binary failed: ${it.stackTraceToString()}\")\n                    return@withContext null\n                }\n\n                return@withContext VideoShot(\n                    times = times.drop(1),\n                    images = images,\n                    imageCountX = videoShot.imgXLen,\n                    imageCountY = videoShot.imgYLen,\n                    imageWidth = videoShot.imgXSize,\n                    imageHeight = videoShot.imgYSize\n                )\n            }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/season/Episode.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video.season\n\nimport dev.aaa1115910.biliapi.entity.video.Dimension\nimport dev.aaa1115910.biliapi.entity.video.VideoPage\n\n/**\n * 剧集视频\n *\n * @param id 剧集id\n * @param aid av号\n * @param bvid bv号\n * @param cid cid\n * @param epid epid\n * @param title 标题 在投稿视频中显示为分 p 标题；\n * 在剧集中，如果存在完整标题，则该标题内容为纯数字，用于显示“第 x 话”，若完整标题内容为空时，显示该分集标题，例如“正片”“中文”\n * @param longTitle 完整标题，仅在剧集中存在，如果不存在完整标题，则该标题为空\n * @param cover 封面\n * @param duration 时长\n * @param dimension 分辨率\n */\ndata class Episode(\n    val id: Int,\n    val aid: Long,\n    val bvid: String,\n    val cid: Long,\n    val epid: Int? = null,\n    val title: String,\n    val longTitle: String,\n    val cover: String,\n    val duration: Int,\n    val pubDate: Long = 0,\n    val dimension: Dimension?,\n    val pages: List<VideoPage>\n) {\n    companion object {\n        fun fromEpisode(episode: bilibili.app.view.v1.Episode) = Episode(\n            id = episode.id.toInt(),\n            aid = episode.aid,\n            bvid = episode.bvid,\n            cid = episode.cid,\n            title = episode.title,\n            longTitle = episode.title,\n            cover = episode.cover,\n            duration = episode.page.duration.toInt(),\n            dimension = Dimension.fromDimension(episode.page.dimension),\n            pages = episode.pagesList.map { VideoPage.fromPage(it) }\n        )\n\n        fun fromEpisode(episode: dev.aaa1115910.biliapi.http.entity.video.UgcSeason.Section.Episode) =\n            Episode(\n                id = episode.id,\n                aid = episode.aid,\n                bvid = episode.bvid,\n                cid = episode.cid,\n                title = episode.title,\n                longTitle = episode.title,\n                cover = episode.arc.pic,\n                duration = episode.arc.duration,\n                pubDate = episode.arc.pubDate.toLong(),\n                dimension = Dimension.fromDimension(episode.page.dimension),\n                pages = episode.pages.map { VideoPage.fromVideoPage(it) }\n            )\n\n        fun fromEpisode(episode: dev.aaa1115910.biliapi.http.entity.season.Episode) = Episode(\n            id = episode.id,\n            aid = episode.aid,\n            cid = episode.cid,\n            bvid = episode.bvid,\n            cover = episode.cover,\n            title = episode.title,\n            longTitle = episode.longTitle,\n            epid = episode.epId,\n            duration = episode.duration,\n            pubDate = episode.pubTime,\n            dimension = episode.dimension?.let { Dimension.fromDimension(it) },\n            pages = emptyList()\n        )\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/season/PgcSeason.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video.season\n\nimport dev.aaa1115910.biliapi.http.entity.season.OtherSeason\n\ndata class UgcSeason(\n    val id: Int,\n    val title: String,\n    val cover: String,\n    val sections: List<Section>\n) {\n    companion object {\n        fun fromUgcSeason(ugcSeason: bilibili.app.view.v1.UgcSeason) = UgcSeason(\n            id = ugcSeason.id.toInt(),\n            title = ugcSeason.title,\n            cover = ugcSeason.cover,\n            sections = ugcSeason.sectionsList.map { Section.fromSection(it) }\n        )\n\n        fun fromUgcSeason(ugcSeason: dev.aaa1115910.biliapi.http.entity.video.UgcSeason) =\n            UgcSeason(\n                id = ugcSeason.id,\n                title = ugcSeason.title,\n                cover = ugcSeason.cover,\n                sections = ugcSeason.sections.map { Section.fromSection(it) }\n            )\n    }\n}\n\n/**\n * 剧集信息\n *\n * @param seasonId 剧集id\n * @param title 剧集标题，仅 App 端\n * @param shortTitle 剧集短标题，用于 TabRow 处显示\n */\ndata class PgcSeason(\n    val seasonId: Int,\n    val title: String?,\n    val shortTitle: String,\n    val cover: String,\n    val horizontalCover: String?\n) {\n    companion object {\n        fun fromSeason(season: OtherSeason): PgcSeason {\n            return PgcSeason(\n                seasonId = season.seasonId,\n                title = season.title,\n                shortTitle = season.seasonTitle,\n                cover = season.cover,\n                horizontalCover = season.horizontalCover ?: season.newEp.cover\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/season/SeasonDetail.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video.season\n\nimport dev.aaa1115910.biliapi.entity.video.VideoDetail.PlayerIcon\nimport dev.aaa1115910.biliapi.http.entity.season.AppSeasonData\nimport dev.aaa1115910.biliapi.http.entity.season.WebSeasonData\n\n/**\n * 剧集详细信息\n *\n * @param title 剧集标题\n * @param originTitle 剧集原始标题，仅出现在具有外文名时，且仅 App 端有此字段\n * @param styles 剧集风格\n * @param cover 封面\n * @param description 剧集简介\n * @param subType 剧集类型，需要用到上报播放记录\n * @param seasonId 剧集id\n * @param userStatus 用户信息，例如购买记录，追剧情况，播放记录等\n * @param publish 发布状态\n * @param newEpDesc 最新一集的描述,\n * @param seasons 同系列剧集列表,\n * @param episodes 剧集视频列表\n * @param sections 相关视频列表\n */\ndata class SeasonDetail(\n    val title: String,\n    val originTitle: String? = null,\n    val styles: List<String>,\n    val cover: String,\n    val description: String,\n    val subType: Int,\n    val seasonId: Int,\n    val userStatus: UserStatus,\n    val publish: Publish,\n    val newEpDesc: String = \"\",\n    val seasons: List<PgcSeason> = emptyList(),\n    val episodes: List<Episode> = emptyList(),\n    val sections: List<Section> = emptyList(),\n    var playerIcon: PlayerIcon? = null\n) {\n    companion object {\n        fun fromSeasonData(seasonData: WebSeasonData): SeasonDetail {\n            return SeasonDetail(\n                title = seasonData.title,\n                originTitle = null,\n                styles = seasonData.styles,\n                cover = seasonData.cover,\n                description = seasonData.evaluate,\n                subType = seasonData.type,\n                seasonId = seasonData.seasonId,\n                userStatus = UserStatus.fromUserStatus(seasonData.userStatus),\n                publish = Publish.fromPublish(seasonData.publish),\n                newEpDesc = seasonData.newEp.desc,\n                seasons = seasonData.seasons.map { PgcSeason.fromSeason(it) },\n                episodes = seasonData.episodes.map { Episode.fromEpisode(it) },\n                sections = seasonData.section.map { Section.fromSection(it) }\n                    .filter { it.episodes.isNotEmpty() }    // 过滤掉跳转别的 pgc 的视频后可能出现空列表\n            )\n        }\n\n        fun fromSeasonData(seasonData: AppSeasonData): SeasonDetail {\n            return SeasonDetail(\n                title = seasonData.title,\n                originTitle = seasonData.originName,\n                styles = seasonData.styles.map { it.name },\n                cover = seasonData.cover,\n                description = seasonData.evaluate,\n                subType = seasonData.type,\n                seasonId = seasonData.seasonId,\n                userStatus = UserStatus.fromUserStatus(seasonData.userStatus),\n                publish = Publish.fromPublish(seasonData.publish),\n                newEpDesc = seasonData.newEp.desc,\n                seasons = seasonData.modules\n                    .firstOrNull { it.style == \"season\" }\n                    ?.data?.seasons\n                    ?.map { PgcSeason.fromSeason(it) }\n                    ?: emptyList(),\n                episodes = seasonData.modules\n                    .firstOrNull { it.style == \"positive\" }\n                    ?.data?.episodes\n                    ?.map { Episode.fromEpisode(it) }\n                    ?: emptyList(),\n                sections = seasonData.modules\n                    .filter { it.style == \"section\" }\n                    .map { Section.fromModule(it) },\n                playerIcon = PlayerIcon.fromPlayerIcon(seasonData.playerIcon)\n            )\n        }\n    }\n\n    /**\n     * 用户信息\n     *\n     * @param follow 已追剧\n     * @param pay 已购买\n     * @param progress 观看记录\n     */\n    data class UserStatus(\n        val follow: Boolean,\n        val pay: Boolean,\n        val progress: Progress? = null\n    ) {\n        companion object {\n            fun fromUserStatus(userStatus: WebSeasonData.UserStatus): UserStatus {\n                return UserStatus(\n                    follow = userStatus.follow == 1,\n                    pay = userStatus.pay == 1,\n                    progress = userStatus.progress?.let {\n                        Progress(\n                            lastEpId = it.lastEpId,\n                            lastEpIndex = it.lastEpIndex,\n                            lastTime = it.lastTime\n                        )\n                    }\n                )\n            }\n\n            fun fromUserStatus(userStatus: AppSeasonData.UserStatus): UserStatus {\n                return UserStatus(\n                    follow = userStatus.follow == 1,\n                    pay = userStatus.pay == 1,\n                    progress = userStatus.progress?.let {\n                        Progress(\n                            lastEpId = it.lastEpId,\n                            lastEpIndex = it.lastEpIndex,\n                            lastTime = it.lastTime\n                        )\n                    }\n                )\n            }\n        }\n\n        /**\n         * 观看记录\n         *\n         * @param lastEpId 最后观看的epid\n         * @param lastEpIndex 最后观看的ep标题\n         * @param lastTime 最后观看的时间（秒）\n         */\n        data class Progress(\n            val lastEpId: Int,\n            val lastEpIndex: String,\n            val lastTime: Int\n        )\n    }\n\n    data class Publish(\n        val isPublished: Boolean,\n        val publishDate: String\n    ) {\n        companion object {\n            fun fromPublish(publish: dev.aaa1115910.biliapi.http.entity.season.Publish): Publish {\n                return Publish(\n                    isPublished = publish.isStarted,\n                    publishDate = publish.pubTimeShow\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/entity/video/season/Section.kt",
    "content": "package dev.aaa1115910.biliapi.entity.video.season\n\ndata class Section(\n    val id: Long,\n    val title: String,\n    val episodes: List<Episode>\n) {\n    companion object {\n        fun fromSection(section: dev.aaa1115910.biliapi.http.entity.video.UgcSeason.Section) =\n            Section(\n                id = section.id,\n                title = section.title,\n                episodes = section.episodes.map { Episode.fromEpisode(it) }\n            )\n\n        fun fromSection(section: bilibili.app.view.v1.Section) = Section(\n            id = section.id,\n            title = section.title,\n            episodes = section.episodesList.map { Episode.fromEpisode(it) }\n        )\n\n        fun fromModule(module: dev.aaa1115910.biliapi.http.entity.season.AppSeasonData.Module) =\n            Section(\n                id = module.id,\n                title = module.title,\n                episodes = module.data.episodes.map { Episode.fromEpisode(it) }\n            )\n\n        fun fromSection(section: dev.aaa1115910.biliapi.http.entity.season.SeasonSection) =\n            Section(\n                id = section.id,\n                title = section.title,\n                episodes = section.episodes.map { Episode.fromEpisode(it) }\n                    .filter { it.aid != 0L }    // aid 为 0 的视频是跳转到其它 PGC 页面的“链接”，暂不适配（\n            )\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/grpc/utils/Channel.kt",
    "content": "package dev.aaa1115910.biliapi.grpc.utils\n\nimport bilibili.metadata.device.device\nimport bilibili.metadata.locale.locale\nimport bilibili.metadata.metadata\nimport bilibili.metadata.network.NetworkType\nimport bilibili.metadata.network.network\nimport dev.aaa1115910.biliapi.http.util.BiliAppConf\nimport io.grpc.CallOptions\nimport io.grpc.Channel\nimport io.grpc.ClientCall\nimport io.grpc.ClientInterceptor\nimport io.grpc.ForwardingClientCall.SimpleForwardingClientCall\nimport io.grpc.ManagedChannel\nimport io.grpc.ManagedChannelBuilder\nimport io.grpc.MethodDescriptor\nimport io.ktor.client.request.HttpRequestBuilder\nimport io.ktor.client.request.header\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.asExecutor\nimport kotlin.io.encoding.Base64\nimport kotlin.io.encoding.ExperimentalEncodingApi\nimport io.grpc.Metadata as GrpcMetadata\n\nfun generateChannel(\n    accessKey: String,\n    buvid: String,\n    endPoint: String = BiliAppConf.GRPC_HOST,\n    port: Int = BiliAppConf.GRPC_PORT,\n    enableTransportSecurity: Boolean = true\n): ManagedChannel = ManagedChannelBuilder\n    .forAddress(endPoint, port)\n    .apply { if (enableTransportSecurity) useTransportSecurity() else usePlaintext() }\n    .executor(Dispatchers.IO.asExecutor())\n    .intercept(MetadataInterceptor(accessKey, buvid))\n    .build()\n\nprivate class MetadataInterceptor(\n    private val accessKey: String,\n    private val buvid: String\n) : ClientInterceptor {\n    override fun <ReqT, RespT> interceptCall(\n        method: MethodDescriptor<ReqT, RespT>, callOptions: CallOptions, next: Channel\n    ): ClientCall<ReqT, RespT> {\n        return object : SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {\n            override fun start(responseListener: Listener<RespT>, headers: GrpcMetadata) {\n                headers.apply {\n                    putAuthorization(accessKey)\n                    putMetadataBin(accessKey, buvid)\n                    putDeviceBin(buvid)\n                    putLocalBin()\n                    putNetworkBin()\n                }\n                super.start(responseListener, headers)\n            }\n        }\n    }\n}\n\nfun GrpcMetadata.putAuthorization(accessKey: String) {\n    put(\n        GrpcMetadata.Key.of(\"authorization\", GrpcMetadata.ASCII_STRING_MARSHALLER),\n        \"identify_v1 $accessKey\"\n    )\n}\n\nfun GrpcMetadata.putMetadataBin(accessKey: String, buvid: String) {\n    put(\n        GrpcMetadata.Key.of(\"x-bili-metadata-bin\", GrpcMetadata.BINARY_BYTE_MARSHALLER),\n        metadata {\n            this.accessKey = accessKey\n            mobiApp = BiliAppConf.MOBI_APP\n            device = BiliAppConf.DEVICE\n            build = BiliAppConf.APP_BUILD_CODE\n            channel = BiliAppConf.CHANNEL\n            this.buvid = buvid\n            platform = BiliAppConf.PLATFORM\n        }.toByteArray()\n    )\n}\n\nfun GrpcMetadata.putDeviceBin(buvid: String) {\n    put(\n        io.grpc.Metadata.Key.of(\"x-bili-device-bin\", GrpcMetadata.BINARY_BYTE_MARSHALLER),\n        device {\n            appId = BiliAppConf.APP_ID\n            mobiApp = BiliAppConf.MOBI_APP\n            device = BiliAppConf.DEVICE\n            build = BiliAppConf.APP_BUILD_CODE\n            channel = BiliAppConf.CHANNEL\n            this.buvid = buvid\n            platform = BiliAppConf.PLATFORM\n        }.toByteArray()\n    )\n}\n\nfun GrpcMetadata.putLocalBin() {\n    put(\n        io.grpc.Metadata.Key.of(\"x-bili-local-bin\", GrpcMetadata.BINARY_BYTE_MARSHALLER),\n        locale {\n            timezone = BiliAppConf.TIMEZONE\n        }.toByteArray()\n    )\n}\n\nfun GrpcMetadata.putNetworkBin() {\n    put(\n        io.grpc.Metadata.Key.of(\"x-bili-network-bin\", GrpcMetadata.BINARY_BYTE_MARSHALLER),\n        network {\n            type = NetworkType.WIFI\n        }.toByteArray()\n    )\n}\n\n@OptIn(ExperimentalEncodingApi::class)\nfun HttpRequestBuilder.generateGrpcProxyHeaders(\n    accessKey: String,\n    buvid: String\n) {\n    header(\"authorization\", \"identify_v1 $accessKey\")\n    header(\"x-bili-metadata-bin\", Base64.encode(metadata {\n        this.accessKey = accessKey\n        mobiApp = BiliAppConf.MOBI_APP\n        device = BiliAppConf.DEVICE\n        build = BiliAppConf.APP_BUILD_CODE\n        channel = BiliAppConf.CHANNEL\n        this.buvid = buvid\n        platform = BiliAppConf.PLATFORM\n    }.toByteArray()))\n    header(\"x-bili-device-bin\", Base64.encode(device {\n        appId = BiliAppConf.APP_ID\n        mobiApp = BiliAppConf.MOBI_APP\n        device = BiliAppConf.DEVICE\n        build = BiliAppConf.APP_BUILD_CODE\n        channel = BiliAppConf.CHANNEL\n        this.buvid = buvid\n        platform = BiliAppConf.PLATFORM\n    }.toByteArray()))\n    header(\"x-bili-local-bin\", Base64.encode(locale {\n        timezone = BiliAppConf.TIMEZONE\n    }.toByteArray()))\n    header(\"x-bili-network-bin\", Base64.encode(network {\n        type = NetworkType.WIFI\n    }.toByteArray()))\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/grpc/utils/StatusExtends.kt",
    "content": "package dev.aaa1115910.biliapi.grpc.utils\n\nimport bilibili.rpc.Status\nimport com.google.protobuf.Message\n//import com.google.rpc.Status\nimport io.grpc.Metadata\nimport io.grpc.StatusException\n\n// 如果没有设置 java_multiple_files = true; 那么就需要使用下面的代码\n// 这时候生成的代码会将很多 class 放在同一个 class 里面\n// 例如 bilibili.rpc.Status 会被放在 bilibili.rpc.StatusOuterClass$Status 中\n// 这时候直接从 typeUrl 中获取 class 名称是不对的\n/*\n@Suppress(\"UNCHECKED_CAST\")\nfun Status.getTypeClass(): Class<Message> {\n    val protoClassName = this.detailsList.first().typeUrl.split(\"/\").last()\n    val splitProtoClassNames = protoClassName.split(\".\")\n    val nameClass = splitProtoClassNames\n        .subList(0, splitProtoClassNames.size - 1)\n        .joinToString(\".\") +\n            \".${splitProtoClassNames.last()}OuterClass$${splitProtoClassNames.last()}\"\n    return Class.forName(nameClass) as Class<Message>\n}\n*/\n\n@Suppress(\"UNCHECKED_CAST\")\nfun Status.getTypeClass(): Class<Message> {\n    val nameClass = this.detailsList.first().typeUrl.split(\"/\").last()\n    return Class.forName(nameClass) as Class<Message>\n}\n\nfun Status.getDetail(): Any {\n    val clazz = this.getTypeClass()\n    return this.detailsList.first().unpack(clazz)\n}\n\nfun handleGrpcException(it: Throwable) {\n    when (it) {\n        is StatusException -> {\n            val statusDetailsKey = Metadata.Key.of(\n                \"grpc-status-details-bin\", Metadata.BINARY_BYTE_MARSHALLER\n            )\n            val data = it.trailers[statusDetailsKey]\n            val status = Status.parseFrom(data).getDetail()\n            when (status) {\n                is bilibili.rpc.Status -> {\n                    throw IllegalStateException(status.message)\n                }\n\n                is common.ErrorProto -> {\n                    throw IllegalStateException(status.message)\n                }\n            }\n        }\n\n        else -> throw it\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/BiliHttpApi.kt",
    "content": "package dev.aaa1115910.biliapi.http\n\nimport com.tfowl.ktor.client.plugins.JsoupPlugin\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.http.BiliHttpApi.getRegionDynamic\nimport dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_WEB\nimport dev.aaa1115910.biliapi.http.entity.BiliResponse\nimport dev.aaa1115910.biliapi.http.entity.BiliResponseWithoutData\nimport dev.aaa1115910.biliapi.http.entity.VVoucherException\nimport dev.aaa1115910.biliapi.http.entity.danmaku.DanmakuData\nimport dev.aaa1115910.biliapi.http.entity.danmaku.DanmakuResponse\nimport dev.aaa1115910.biliapi.http.entity.dynamic.DynamicData\nimport dev.aaa1115910.biliapi.http.entity.dynamic.DynamicDetailData\nimport dev.aaa1115910.biliapi.http.entity.history.HistoryData\nimport dev.aaa1115910.biliapi.http.entity.home.RcmdIndexData\nimport dev.aaa1115910.biliapi.http.entity.home.RcmdTopData\nimport dev.aaa1115910.biliapi.http.entity.index.IndexResultData\nimport dev.aaa1115910.biliapi.entity.pgc.index.PgcIndexConditionData\nimport dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedData\nimport dev.aaa1115910.biliapi.http.entity.pgc.PgcFeedV3Data\nimport dev.aaa1115910.biliapi.http.entity.pgc.PgcWebInitialStateData\nimport dev.aaa1115910.biliapi.http.entity.region.RegionBanner\nimport dev.aaa1115910.biliapi.http.entity.region.RegionDynamic\nimport dev.aaa1115910.biliapi.http.entity.region.RegionDynamicList\nimport dev.aaa1115910.biliapi.http.entity.region.RegionFeedRcmd\nimport dev.aaa1115910.biliapi.http.entity.region.RegionLocs\nimport dev.aaa1115910.biliapi.http.entity.reply.CommentData\nimport dev.aaa1115910.biliapi.http.entity.reply.CommentReplyData\nimport dev.aaa1115910.biliapi.http.entity.search.AppSearchSquareData\nimport dev.aaa1115910.biliapi.http.entity.search.KeywordSuggest\nimport dev.aaa1115910.biliapi.http.entity.search.SearchResultData\nimport dev.aaa1115910.biliapi.http.entity.search.SearchTendingData\nimport dev.aaa1115910.biliapi.http.entity.search.WebSearchSquareData\nimport dev.aaa1115910.biliapi.http.entity.season.AppSeasonData\nimport dev.aaa1115910.biliapi.http.entity.season.FollowingSeasonAppData\nimport dev.aaa1115910.biliapi.http.entity.season.FollowingSeasonWebData\nimport dev.aaa1115910.biliapi.http.entity.season.SeasonFollowData\nimport dev.aaa1115910.biliapi.http.entity.season.WebSeasonData\nimport dev.aaa1115910.biliapi.http.entity.toview.ToViewData\nimport dev.aaa1115910.biliapi.http.entity.user.AppSpaceVideoData\nimport dev.aaa1115910.biliapi.http.entity.user.FollowAction\nimport dev.aaa1115910.biliapi.http.entity.user.FollowActionSource\nimport dev.aaa1115910.biliapi.http.entity.user.MyInfoData\nimport dev.aaa1115910.biliapi.http.entity.user.RelationData\nimport dev.aaa1115910.biliapi.http.entity.user.RelationStat\nimport dev.aaa1115910.biliapi.http.entity.user.UserCardData\nimport dev.aaa1115910.biliapi.http.entity.user.UserFollowData\nimport dev.aaa1115910.biliapi.http.entity.user.UserInfoData\nimport dev.aaa1115910.biliapi.http.entity.user.WebSpaceVideoData\nimport dev.aaa1115910.biliapi.http.entity.user.favorite.FavoriteFolderInfo\nimport dev.aaa1115910.biliapi.http.entity.user.favorite.FavoriteFolderInfoListData\nimport dev.aaa1115910.biliapi.http.entity.user.favorite.FavoriteItemIdListResponse\nimport dev.aaa1115910.biliapi.http.entity.user.favorite.UserFavoriteFoldersData\nimport dev.aaa1115910.biliapi.http.entity.user.garb.Equip\nimport dev.aaa1115910.biliapi.http.entity.user.garb.EquipPart\nimport dev.aaa1115910.biliapi.http.entity.video.AddCoin\nimport dev.aaa1115910.biliapi.http.entity.video.ArchiveRelation\nimport dev.aaa1115910.biliapi.http.entity.video.CheckSentCoin\nimport dev.aaa1115910.biliapi.http.entity.video.CheckVideoFavoured\nimport dev.aaa1115910.biliapi.http.entity.video.GaiaVgateRegisterData\nimport dev.aaa1115910.biliapi.http.entity.video.GaiaVgateValidateData\nimport dev.aaa1115910.biliapi.http.entity.video.PlayUrlData\nimport dev.aaa1115910.biliapi.http.entity.video.PlayUrlV2Data\nimport dev.aaa1115910.biliapi.http.entity.video.PopularVideoData\nimport dev.aaa1115910.biliapi.http.entity.video.RelatedVideosResponse\nimport dev.aaa1115910.biliapi.http.entity.video.SetVideoFavorite\nimport dev.aaa1115910.biliapi.http.entity.video.Tag\nimport dev.aaa1115910.biliapi.http.entity.video.TagDetail\nimport dev.aaa1115910.biliapi.http.entity.video.TagTopVideosResponse\nimport dev.aaa1115910.biliapi.http.entity.video.Timeline\nimport dev.aaa1115910.biliapi.http.entity.video.TimelineAppData\nimport dev.aaa1115910.biliapi.http.entity.video.VideoDetail\nimport dev.aaa1115910.biliapi.http.entity.video.VideoInfo\nimport dev.aaa1115910.biliapi.http.entity.video.VideoMoreInfo\nimport dev.aaa1115910.biliapi.http.entity.video.VideoOnlineTotal\nimport dev.aaa1115910.biliapi.http.entity.video.VideoShot\nimport dev.aaa1115910.biliapi.http.entity.web.NavResponseData\nimport dev.aaa1115910.biliapi.http.plugins.BiliUserAgent\nimport dev.aaa1115910.biliapi.http.util.BiliAppConf\nimport dev.aaa1115910.biliapi.http.util.BiliDns\nimport dev.aaa1115910.biliapi.http.util.encApiSign\nimport dev.aaa1115910.biliapi.http.util.skipAddBuvid3Cookie\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.HttpRequestRetry\nimport io.ktor.client.plugins.compression.ContentEncoding\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.request.forms.FormDataContent\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport io.ktor.client.request.post\nimport io.ktor.client.request.setBody\nimport io.ktor.client.statement.bodyAsChannel\nimport io.ktor.client.statement.bodyAsText\nimport io.ktor.client.statement.readRawBytes\nimport io.ktor.http.Parameters\nimport io.ktor.http.URLProtocol\nimport io.ktor.serialization.kotlinx.json.json\nimport io.ktor.utils.io.InternalAPI\nimport io.ktor.utils.io.jvm.javaio.toInputStream\nimport kotlinx.coroutines.CoroutineScope\nimport bilibili.community.service.dm.v1.DmSegMobileReply\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.contentOrNull\nimport kotlinx.serialization.json.decodeFromJsonElement\nimport kotlinx.serialization.json.jsonObject\nimport kotlinx.serialization.json.jsonPrimitive\nimport org.jsoup.nodes.Document\nimport java.util.concurrent.ConcurrentHashMap\nimport javax.xml.parsers.DocumentBuilderFactory\n\n@Suppress(\"SpellCheckingInspection\")\nobject BiliHttpApi {\n    private var endPoint: String = \"api.bilibili.com\"\n    private lateinit var client: HttpClient\n\n    // 用于获取 sessData 的提供者，由应用层设置\n    var sessDataProvider: () -> String = { \"\" }\n    // 用于获取 buvid3 的提供者，由应用层设置\n    var buvid3Provider: () -> String? = { null }\n\n    private val json = Json {\n        coerceInputValues = true\n        ignoreUnknownKeys = true\n        prettyPrint = true\n    }\n\n    var wbiImgKey: String? = null\n    var wbiSubKey: String? = null\n    private var wbiLastRefreshDate = 0L\n\n    // 缓存相关变量\n    private data class CacheEntry<T>(\n        val data: T,\n        var expireTime: Long\n    )\n\n    private val videoMoreInfoCache = ConcurrentHashMap<String, CacheEntry<BiliResponse<VideoMoreInfo>>>()\n\n    init {\n        createClient()\n        CoroutineScope(Dispatchers.IO).launch {\n            updateWbi()\n        }\n    }\n\n    private fun createClient() {\n        client = HttpClient(OkHttp) {\n            engine {\n                config {\n                    dns(BiliDns)\n                }\n            }\n            BiliUserAgent()\n            install(ContentNegotiation) {\n                json(json)\n            }\n            install(ContentEncoding) {\n                deflate(1.0F)\n                gzip(0.9F)\n            }\n            install(HttpRequestRetry) {\n                retryOnException(maxRetries = 2)\n            }\n            install(JsoupPlugin)\n            defaultRequest {\n                url {\n                    host = endPoint\n                    protocol = URLProtocol.HTTPS\n                }\n            }\n        }.apply {\n            encApiSign()\n        }\n    }\n\n    /**\n     * 检查响应体是否包含风控 v_voucher。\n     *\n     * 当 API 返回 `{\"code\":0,\"data\":{\"v_voucher\":\"voucher_xxx\"}}` 时，\n     * 表示触发了风控，需要通过 Geetest 验证，此方法会抛出 [VVoucherException]。\n     */\n    private fun checkForVVoucher(bodyText: String) {\n        runCatching {\n            val root = Json.parseToJsonElement(bodyText).jsonObject\n            val code = root[\"code\"]?.jsonPrimitive?.contentOrNull?.toIntOrNull() ?: return\n            if (code != 0) return\n            val data = root[\"data\"]?.jsonObject ?: root[\"result\"]?.jsonObject ?: return\n            val vVoucher = data[\"v_voucher\"]?.jsonPrimitive?.contentOrNull\n            if (!vVoucher.isNullOrBlank()) {\n                throw VVoucherException(vVoucher)\n            }\n        }.onFailure {\n            if (it is VVoucherException) throw it\n            // JSON 解析失败不影响正常流程\n        }\n    }\n\n    /**\n     * 获取热门视频列表\n     */\n    suspend fun getPopularVideoData(\n        pageNumber: Int = 1,\n        pageSize: Int = 20,\n        sessData: String = \"\"\n    ): BiliResponse<PopularVideoData> = client.get(\"/x/web-interface/popular\") {\n        parameter(\"pn\", pageNumber)\n        parameter(\"ps\", pageSize)\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 获取视频详细信息\n     */\n    suspend fun getVideoInfo(\n        av: Int? = null,\n        bv: String? = null,\n        sessData: String? = null\n    ): BiliResponse<VideoInfo> = client.get(\"/x/web-interface/view\") {\n        parameter(\"aid\", av)\n        parameter(\"bvid\", bv)\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$sessData;\") }\n    }.body()\n\n    /**\n     * 获取视频超详细信息\n     */\n    suspend fun getVideoDetail(\n        av: Long? = null,\n        bv: String? = null,\n        sessData: String? = null\n    ): BiliResponse<VideoDetail> {\n        val response = client.get(\"/x/web-interface/wbi/view/detail\") {\n            av?.let { parameter(\"aid\", av) }\n            bv?.let { parameter(\"bvid\", bv) }\n\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$sessData;\") }\n            skipAddBuvid3Cookie()\n        }\n        println(\"getVideoDetail:\" + response.bodyAsText())\n        return response.body()\n    }\n\n    /**\n     * 获取视频流\n     */\n    suspend fun getVideoPlayUrl(\n        av: Long? = null,\n        bv: String? = null,\n        cid: Long,\n        qn: Int? = 80,\n        fnval: Int? = 1,\n        fnver: Int? = 0,\n        fourk: Int? = 1,\n        session: String? = null,\n        otype: String = \"json\",\n        type: String = \"\",\n        platform: String = \"oc\",\n        sessData: String? = null,\n        dedeUserID: Long? = null,\n        gaiaVtoken: String? = null\n    ): BiliResponse<PlayUrlData> {\n        val response = client.get(\"/x/player/wbi/playurl\") {\n            require(av != null || bv != null) { \"av and bv cannot be null at the same time\" }\n            parameter(\"avid\", av)\n            parameter(\"bvid\", bv)\n            parameter(\"cid\", cid)\n            parameter(\"qn\", qn)\n            parameter(\"fnval\", fnval)\n            parameter(\"fnver\", fnver)\n            parameter(\"fourk\", fourk)\n            parameter(\"session\", session)\n            parameter(\"otype\", otype)\n            parameter(\"type\", type)\n            parameter(\"platform\", platform)\n            gaiaVtoken?.let { parameter(\"gaia_vtoken\", it) }\n            if (sessData.isNullOrEmpty()) {\n                // parameter(\"voice_balance\", 1)\n                parameter(\"web_location\", \"1315873\")\n                parameter(\"gaia_source\", \"pre-load\")\n                parameter(\"isGaiaAvoided\", \"true\")\n                parameter(\"try_look\", \"1\")\n            }\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$sessData;DedeUserID=$dedeUserID\") }\n        }\n        val bodyText = response.bodyAsText()\n        // println(bodyText)\n        checkForVVoucher(bodyText)\n        return json.decodeFromString(bodyText)\n    }\n\n    /**\n     * 获取剧集视频流\n     */\n    suspend fun getPgcVideoPlayUrl(\n        av: Long? = null,\n        bv: String? = null,\n        epid: Int? = null,\n        cid: Long? = null,\n        qn: Int? = null,\n        fnval: Int? = null,\n        fnver: Int? = null,\n        fourk: Int? = null,\n        session: String? = null,\n        supportMultiAudio: Boolean? = null,\n        drmTechType: Int? = null,\n        fromClient: String? = null,\n        sessData: String? = null,\n        dedeUserID: Long? = null,\n        buvid3: String? = null,\n        gaiaVtoken: String? = null\n    ): BiliResponse<PlayUrlData> {\n        val response = client.get(\"/pgc/player/web/playurl\") {\n            require(av != null || bv != null) { \"av and bv cannot be null at the same time\" }\n            require(epid != null || cid != null) { \"epid and cid cannot be null at the same time\" }\n            av?.let { parameter(\"avid\", it) }\n            bv?.let { parameter(\"bvid\", it) }\n            epid?.let { parameter(\"ep_id\", it) }\n            cid?.let { parameter(\"cid\", it) }\n            qn?.let { parameter(\"qn\", it) }\n            fnval?.let { parameter(\"fnval\", it) }\n            fnver?.let { parameter(\"fnver\", it) }\n            fourk?.let { parameter(\"fourk\", it) }\n            session?.let { parameter(\"session\", it) }\n            supportMultiAudio?.let { parameter(\"support_multi_audio\", it) }\n            drmTechType?.let { parameter(\"drm_tech_type\", it) }\n            fromClient?.let { parameter(\"from_client\", it) }\n            gaiaVtoken?.let { parameter(\"gaia_vtoken\", it) }\n            val cookieParts = mutableListOf<String>()\n            sessData?.let { cookieParts.add(\"SESSDATA=$it\") }\n            dedeUserID?.let { cookieParts.add(\"DedeUserID=$it\") }\n            buvid3?.let { cookieParts.add(\"buvid3=$it\") }\n            if (cookieParts.isNotEmpty()) header(\"Cookie\", cookieParts.joinToString(\";\"))\n            //必须得加上 referer 才能通过账号身份验证\n            header(\"referer\", \"https://www.bilibili.com\")\n        }\n        val bodyText = response.bodyAsText()\n        checkForVVoucher(bodyText)\n        return json.decodeFromString(bodyText)\n    }\n\n    /**\n     * 获取剧集视频流 v2\n     */\n    suspend fun getPgcVideoPlayUrlV2(\n        av: Long? = null,\n        bv: String? = null,\n        epid: Int? = null,\n        cid: Long? = null,\n        qn: Int? = null,\n        fnval: Int? = null,\n        fnver: Int? = null,\n        fourk: Int? = null,\n        session: String? = null,\n        supportMultiAudio: Boolean? = null,\n        drmTechType: Int? = null,\n        fromClient: String? = null,\n        sessData: String? = null,\n        buvid3: String? = null,\n        gaiaVtoken: String? = null\n    ): BiliResponse<PlayUrlV2Data> {\n        val response = client.get(\"/pgc/player/web/v2/playurl\") {\n            av?.let { parameter(\"avid\", it) }\n            bv?.let { parameter(\"bvid\", it) }\n            epid?.let { parameter(\"ep_id\", it) }\n            cid?.let { parameter(\"cid\", it) }\n            qn?.let { parameter(\"qn\", it) }\n            fnval?.let { parameter(\"fnval\", it) }\n            fnver?.let { parameter(\"fnver\", it) }\n            fourk?.let { parameter(\"fourk\", it) }\n            session?.let { parameter(\"session\", it) }\n            supportMultiAudio?.let { parameter(\"support_multi_audio\", it) }\n            drmTechType?.let { parameter(\"drm_tech_type\", it) }\n            fromClient?.let { parameter(\"from_client\", it) }\n            gaiaVtoken?.let { parameter(\"gaia_vtoken\", it) }\n            val cookieParts = mutableListOf<String>()\n            sessData?.let { cookieParts.add(\"SESSDATA=$it\") }\n            buvid3?.let { cookieParts.add(\"buvid3=$it\") }\n            if (cookieParts.isNotEmpty()) {\n                val cookieString = cookieParts.joinToString(\";\")\n                println(\"PGC v2 Cookie: $cookieString\")\n                header(\"Cookie\", cookieString)\n            } else {\n                println(\"PGC v2 Cookie is empty! sessData=$sessData, buvid3=$buvid3\")\n            }\n            //必须得加上 referer 才能通过账号身份验证\n            header(\"referer\", \"https://www.bilibili.com\")\n        }\n        val bodyText = response.bodyAsText()\n        checkForVVoucher(bodyText)\n        return json.decodeFromString(bodyText)\n    }\n\n    /**\n     * 通过[cid]获取视频弹幕\n     */\n    suspend fun getDanmakuXml(\n        cid: Long,\n        sessData: String = \"\"\n    ): DanmakuResponse {\n        val xmlChannel = client.get(\"/x/v1/dm/list.so\") {\n            parameter(\"oid\", cid)\n            header(\"Cookie\", \"SESSDATA=$sessData;\")\n        }.bodyAsChannel()\n\n        val dbFactory = DocumentBuilderFactory.newInstance()\n        val dBuilder = dbFactory.newDocumentBuilder()\n        val doc = withContext(Dispatchers.IO) {\n            dBuilder.parse(xmlChannel.toInputStream())\n        }\n        doc.documentElement.normalize()\n\n        val chatServer = doc.getElementsByTagName(\"chatserver\").item(0).textContent\n        val chatId = doc.getElementsByTagName(\"chatid\").item(0).textContent.toLong()\n        val maxLimit = doc.getElementsByTagName(\"maxlimit\").item(0).textContent.toInt()\n        val state = doc.getElementsByTagName(\"state\").item(0).textContent.toInt()\n        val realName = doc.getElementsByTagName(\"real_name\").item(0).textContent.toInt()\n        val source = runCatching {\n            doc.getElementsByTagName(\"source\").item(0).textContent\n        }.getOrDefault(\"\")\n\n        val data = mutableListOf<DanmakuData>()\n        val danmakuNodes = doc.getElementsByTagName(\"d\")\n\n        for (i in 0 until danmakuNodes.length) {\n            val danmakuNode = danmakuNodes.item(i)\n            val p = danmakuNode.attributes.item(0).textContent\n            val text = danmakuNode.textContent\n            data.add(DanmakuData.fromString(p, text))\n        }\n\n        return DanmakuResponse(chatServer, chatId, maxLimit, state, realName, source, data)\n    }\n\n    /**\n     * 通过[cid]和[avid]获取视频弹幕\n     * 支持分段获取\n     *\n     * @param cid 视频 cid\n     * @param avid 视频 avid\n     * @param segmentIndex 分段索引，从 1 开始。每 6min 一包\n     * @param sessData 用户认证 cookie\n     * @return 弹幕数据列表\n     */\n    suspend fun getDanmakuSeg(\n        cid: Long,\n        avid: Long,\n        segmentIndex: Int = 1,\n        sessData: String = \"\"\n    ): List<DanmakuData> {\n        val responseBytes = client.get(\"/x/v2/dm/wbi/web/seg.so\") {\n            parameter(\"type\", 1) // 1:视频\n            parameter(\"oid\", cid)\n            parameter(\"pid\", avid)\n            parameter(\"segment_index\", segmentIndex)\n            header(\"Cookie\", \"SESSDATA=$sessData;\")\n        }.readRawBytes()\n\n        val reply = bilibili.community.service.dm.v1.DmSegMobileReply.parseFrom(responseBytes)\n\n        return reply.elemsList.map { elem ->\n            DanmakuData(\n                time = elem.progress / 1000f, // ms -> s\n                type = elem.mode,\n                size = elem.fontsize,\n                color = elem.color,\n                timestamp = (elem.ctime / 1000).toInt(), // ms -> s\n                pool = elem.pool,\n                midHash = elem.midHash,\n                dmid = elem.id,\n                level = elem.weight, // weight 用于屏蔽等级\n                text = elem.content\n            )\n        }\n    }\n\n    /**\n     * 获取动态列表\n     *\n     * @param type 返回数据额类型 all:全部 video:视频投稿 pgc:追番追剧 article：专栏\n     * @param offset 请求第2页及其之后时填写，填写上一次请求获得的offset\n     */\n    suspend fun getDynamicList(\n        timezoneOffset: Int = -480,\n        type: String = \"all\",\n        page: Int = 1,\n        offset: String? = null,\n        sessData: String = \"\"\n    ): BiliResponse<DynamicData> = client.get(\"/x/polymer/web-dynamic/v1/feed/all\") {\n        parameter(\"timezone_offset\", timezoneOffset)\n        parameter(\"type\", type)\n        parameter(\"page\", page)\n        offset?.let { parameter(\"offset\", offset) }\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 获取动态详情\n     *\n     * @param id 动态id\n     */\n    suspend fun getDynamicDetail(\n        timezoneOffset: Int = -480,\n        id: String,\n        features: String? = null,\n        sessData: String = \"\"\n    ): BiliResponse<DynamicDetailData> = client.get(\"/x/polymer/web-dynamic/v1/detail\") {\n        parameter(\"timezone_offset\", timezoneOffset)\n        parameter(\"id\", id)\n        features?.let { parameter(\"features\", it) }\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 获取用户[uid]的详细信息\n     */\n    suspend fun getUserInfo(\n        uid: Long,\n        sessData: String = \"\"\n    ): BiliResponse<UserInfoData> = client.get(\"/x/space/wbi/acc/info\") {\n        parameter(\"mid\", uid)\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n\n        // 风控\n        parameter(\"dm_img_list\", \"[]\")\n        parameter(\"dm_img_str\", \"V2ViR0wgMS4wIChPcGVuR0wgRVMgMi4wIENocm9taXVtKQ\")\n        parameter(\n            \"dm_cover_img_str\",\n            \"QU5HTEUgKEFNRCwgQU1EIFJhZGVvbiA3ODBNIEdyYXBoaWNzICgweDAwMDAxNUJGKSBEaXJlY3QzRDExIHZzXzVfMCBwc181XzAsIEQzRDExKUdvb2dsZSBJbmMuIChBTU\"\n        )\n        parameter(\"dm_img_inter\", \"{\\\"ds\\\":[],\\\"wh\\\":[4769,2793,43],\\\"of\\\":[285,570,285]}\")\n        header(\"referer\", \"https://space.bilibili.com\")\n    }.body()\n\n\n    /**\n     * 获取用户[uid]的卡片信息\n     *\n     * @param uid 用户id\n     * @param photo 是否请求用户主页头图\n     */\n    suspend fun getUserCardInfo(\n        mid: Long,\n        photo: Boolean = false,\n        sessData: String = \"\"\n    ): BiliResponse<UserCardData> = client.get(\"/x/web-interface/card\") {\n        parameter(\"mid\", mid)\n        parameter(\"photo\", photo)\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 通过[sessData]获取用户个人信息\n     */\n    suspend fun getUserSelfInfo(\n        buvid3: String? = null,\n        sessData: String = \"\"\n    ): BiliResponse<MyInfoData> = client.get(\"/x/space/myinfo\") {\n        if (buvid3 != null && sessData.isNotEmpty()) {\n            header(\"Cookie\", \"buvid3=$buvid3; SESSDATA=$sessData;\")\n        } else {\n            header(\"Cookie\", \"SESSDATA=$sessData;\")\n        }\n    }.body()\n\n    /**\n     * 获取截止至目标id[max]和目标时间[viewAt]历史记录\n     *\n     * @param business 分类 貌似无效\n     * @param pageSize 页面大小\n     */\n    suspend fun getHistories(\n        max: Long = 0,\n        business: String = \"\",\n        viewAt: Long = 0,\n        pageSize: Int = 20,\n        sessData: String = \"\"\n    ): BiliResponse<HistoryData> = client.get(\"/x/web-interface/history/cursor\") {\n        parameter(\"max\", max)\n        parameter(\"business\", business)\n        parameter(\"view_at\", viewAt)\n        parameter(\"ps\", pageSize)\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 获取稍后再看列表\n     */\n\n    suspend fun getToView(\n        // max: Long = 0,\n        // business: String = \"\",\n        // viewAt: Long = 0,\n        // pageSize: Int = 20,\n        sessData: String = \"\"\n    ): BiliResponse<ToViewData> = client.get(\"/x/v2/history/toview\") {\n        // parameter(\"max\", max)\n        // parameter(\"business\", business)\n        // parameter(\"view_at\", viewAt)\n        // parameter(\"ps\", pageSize)\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 删除历史记录[kid]\n     */\n    suspend fun deleteHistory(\n        kid: String,\n        csrf: String,\n        sessData: String\n    ): BiliResponseWithoutData = client.post(\"/x/v2/history/delete\") {\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    append(\"kid\", kid)\n                    append(\"csrf\", csrf)\n                }\n            )\n        )\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 清空历史记录\n     */\n    suspend fun clearHistory(\n        csrf: String,\n        sessData: String\n    ): BiliResponseWithoutData = client.post(\"/x/v2/history/clear\") {\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    append(\"csrf\", csrf)\n                }\n            )\n        )\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 从稍后再看列表中删除视频[avid]\n     */\n    suspend fun deleteToView(\n        avid: Long,\n        csrf: String,\n        sessData: String\n    ): BiliResponseWithoutData = client.post(\"/x/v2/history/toview/del\") {\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    append(\"aid\", \"$avid\")\n                    append(\"csrf\", csrf)\n                }\n            )\n        )\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 清空稍后再看列表中的视频[avid]\n     */\n    suspend fun clearToView(\n        csrf: String,\n        sessData: String\n    ): BiliResponseWithoutData = client.post(\"/x/v2/history/toview/clear\") {\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    append(\"csrf\", csrf)\n                }\n            )\n        )\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 添加视频到稍后再看列表中[avid]\n     */\n    suspend fun addToView(\n        avid: Long? = null,\n        bvid: String? = null,\n        csrf: String,\n        sessData: String\n    ): BiliResponseWithoutData = client.post(\"/x/v2/history/toview/add\") {\n        require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    avid?.let {\n                        append(\"aid\", \"$avid\")\n                    }\n                    bvid?.let {\n                        append(\"bvid\", bvid)\n                    }\n                    append(\"csrf\", csrf)\n                }\n            )\n        )\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 获取与视频[avid]或[bvid]有关的相关推荐视频\n     */\n    suspend fun getRelatedVideos(\n        avid: Long? = null,\n        bvid: String? = null\n    ): RelatedVideosResponse = client.get(\"/x/web-interface/archive/related\") {\n        require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n        parameter(\"aid\", avid)\n        parameter(\"bvid\", bvid)\n    }.body()\n\n    /**\n     * 获取收藏夹[mediaId]的元数据\n     */\n    suspend fun getFavoriteFolderInfo(\n        mediaId: Long,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): BiliResponse<FavoriteFolderInfo> = client.get(\"/x/v3/fav/folder/info\") {\n        checkToken(accessKey, sessData)\n        parameter(\"media_id\", mediaId)\n        accessKey?.let { parameter(\"access_key\", it) }\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n    }.body()\n\n    /**\n     * 获取用户[mid]的所有收藏夹信息\n     *\n     * @param type 目标内容属性 默认为全部 0：全部 2：视频稿件\n     * @param rid 目标内容id 视频稿件：视频稿件avid\n     */\n    suspend fun getAllFavoriteFoldersInfo(\n        mid: Long,\n        type: Int = 0,\n        rid: Long? = null,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): BiliResponse<UserFavoriteFoldersData> = client.get(\"/x/v3/fav/folder/created/list-all\") {\n        checkToken(accessKey, sessData)\n        parameter(\"up_mid\", mid)\n        parameter(\"type\", type)\n        parameter(\"rid\", rid)\n        accessKey?.let { parameter(\"access_key\", it) }\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n    }.body()\n\n    /**\n     * 获取收藏夹[mediaId]的详细内容\n     *\n     * @param tid 分区tid 默认为全部分区 0：全部分区\n     * @param keyword 搜索关键字\n     * @param order 排序方式 按收藏时间:mtime 按播放量: view 按投稿时间：pubtime\n     * @param type 查询范围 0：当前收藏夹（对应media_id） 1：全部收藏夹\n     * @param pageSize 每页数量 定义域：1-20\n     * @param pageNumber 页码 默认为1\n     * @param platform 平台标识 可为web（影响内容列表类型）\n     */\n    suspend fun getFavoriteList(\n        mediaId: Long,\n        tid: Int = 0,\n        keyword: String? = null,\n        order: String? = null,\n        type: Int = 0,\n        pageSize: Int = 20,\n        pageNumber: Int = 1,\n        platform: String? = null,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): BiliResponse<FavoriteFolderInfoListData> = client.get(\"/x/v3/fav/resource/list\") {\n        checkToken(accessKey, sessData)\n        parameter(\"media_id\", mediaId)\n        parameter(\"tid\", tid)\n        parameter(\"keyword\", keyword)\n        parameter(\"order\", order)\n        parameter(\"type\", type)\n        parameter(\"ps\", pageSize)\n        parameter(\"pn\", pageNumber)\n        parameter(\"platform\", platform)\n        accessKey?.let { parameter(\"access_key\", it) }\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n    }.body()\n\n    /**\n     * 获取收藏夹[mediaId]的全部内容id\n     */\n    suspend fun getFavoriteIdList(\n        mediaId: Long,\n        platform: String? = null,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): FavoriteItemIdListResponse = client.get(\"/x/v3/fav/resource/ids\") {\n        checkToken(accessKey, sessData)\n        parameter(\"media_id\", mediaId)\n        parameter(\"platform\", platform)\n        accessKey?.let { parameter(\"access_key\", it) }\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n    }.body()\n\n    /**\n     * 上报视频播放心跳\n     *\n     * @param avid 稿件avid avid与bvid任选一个\n     * @param bvid 稿件bvid avid与bvid任选一个\n     * @param cid 视频cid 用于识别分P\n     * @param epid 番剧epid\n     * @param sid 番剧ssid\n     * @param mid 当前用户mid\n     * @param playedTime 视频播放进度 单位为秒 默认为0\n     * @param realtime 总计播放时间 单位为秒\n     * @param startTs 开始播放时刻 时间戳\n     * @param type 视频类型 3：投稿视频 4：剧集 10：课程\n     * @param subType 剧集副类型 当type=4时本参数有效 1：番剧 2：电影 3：纪录片 4：国创 5：电视剧 7：综艺\n     * @param dt 2\n     * @param playType 播放动作 0：播放中 1：开始播放 2：暂停 3：继续播放\n     * @param csrf bili_jct\n     * @param sessData SESSDATA\n     */\n    suspend fun sendHeartbeat(\n        avid: Long? = null,\n        bvid: String? = null,\n        cid: Long? = null,\n        epid: Int? = null,\n        sid: Int? = null,\n        mid: Long? = null,\n        playedTime: Int? = null,\n        realtime: Int? = null,\n        startTs: Long? = null,\n        type: Int? = null,\n        subType: Int? = null,\n        dt: Int? = null,\n        playType: Int? = null,\n        csrf: String? = null,\n        sessData: String\n    ): String = client.post(\"/x/click-interface/web/heartbeat\") {\n        require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    avid?.let { append(\"aid\", \"$it\") }\n                    bvid?.let { append(\"bvid\", it) }\n                    cid?.let { append(\"cid\", \"$it\") }\n                    epid?.let { append(\"epid\", \"$it\") }\n                    sid?.let { append(\"sid\", \"$it\") }\n                    mid?.let { append(\"mid\", \"$it\") }\n                    playedTime?.let { append(\"played_time\", \"$it\") }\n                    realtime?.let { append(\"realtime\", \"$it\") }\n                    startTs?.let { append(\"start_ts\", \"$it\") }\n                    type?.let { append(\"type\", \"$it\") }\n                    subType?.let { append(\"sub_type\", \"$it\") }\n                    dt?.let { append(\"dt\", \"$it\") }\n                    playType?.let { append(\"play_type\", \"$it\") }\n                    csrf?.let { append(\"csrf\", it) }\n                }\n            ))\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.bodyAsText()\n\n    suspend fun sendHeartbeat(\n        avid: Long? = null,\n        bvid: String? = null,\n        cid: Long? = null,\n        epid: Int? = null,\n        sid: Int? = null,\n        mid: Long? = null,\n        playedTime: Int? = null,\n        realtime: Int? = null,\n        startTs: Long? = null,\n        type: Int? = null,\n        subType: Int? = null,\n        dt: Int? = null,\n        playType: Int? = null,\n        accessKey: String? = null\n    ): String = client.post(\"/x/v2/history/report\") {\n        require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    avid?.let { append(\"aid\", \"$it\") }\n                    bvid?.let { append(\"bvid\", it) }\n                    cid?.let { append(\"cid\", \"$it\") }\n                    epid?.let { append(\"epid\", \"$it\") }\n                    sid?.let { append(\"sid\", \"$it\") }\n                    mid?.let { append(\"mid\", \"$it\") }\n                    playedTime?.let { append(\"progress\", \"$it\") }\n                    realtime?.let { append(\"realtime\", \"$it\") }\n                    startTs?.let { append(\"start_ts\", \"$it\") }\n                    type?.let { append(\"type\", \"$it\") }\n                    subType?.let { append(\"sub_type\", \"$it\") }\n                    dt?.let { append(\"dt\", \"$it\") }\n                    playType?.let { append(\"play_type\", \"$it\") }\n                    accessKey?.let { append(\"access_key\", it) }\n                }\n            ))\n    }.bodyAsText()\n\n    /**\n     * 获取视频[avid]的[cid]视频更多信息，例如播放进度\n     */\n    suspend fun getVideoMoreInfo(\n        avid: Long,\n        cid: Long,\n        sessData: String,\n        buvid3: String\n    ): BiliResponse<VideoMoreInfo> {\n        val cacheKey = \"$avid-$cid-$sessData\"\n        val currentTime = System.currentTimeMillis()\n\n        // 清理所有过期的缓存数据\n        val iterator = videoMoreInfoCache.entries.iterator()\n        while (iterator.hasNext()) {\n            val entry = iterator.next()\n            if (currentTime > entry.value.expireTime) {\n                iterator.remove()\n            }\n        }\n\n        videoMoreInfoCache[cacheKey]?.let { cacheEntry ->\n            // 缓存存在且有效，重置TTL并返回缓存结果\n            cacheEntry.expireTime = currentTime + 1000L\n            return cacheEntry.data\n        }\n\n        // 发起请求\n        val response: BiliResponse<VideoMoreInfo> = client.get(\"/x/player/wbi/v2\") {\n            parameter(\"aid\", avid)\n            parameter(\"cid\", cid)\n            header(\"Cookie\", \"buvid3=$buvid3; SESSDATA=$sessData;\")\n        }.body()\n\n        // 缓存结果\n        videoMoreInfoCache[cacheKey] = CacheEntry(response, currentTime + 1000L)\n\n        return response\n    }\n\n    /**\n     * 获取视频在线观看人数\n     */\n    suspend fun getVideoOnlineTotal(\n        cid: Long,\n        bvid: String? = null,\n        aid: Long? = null\n    ): BiliResponse<VideoOnlineTotal> = client.get(\"/x/player/online/total\") {\n        require(bvid != null || aid != null) { \"bvid and aid cannot be null at the same time\" }\n        parameter(\"cid\", cid)\n        bvid?.let { parameter(\"bvid\", it) }\n        aid?.let { parameter(\"aid\", it) }\n    }.body()\n\n    /**\n     * 检查视频[avid]或[bvid]是否已点赞&收藏&投币\n     */\n    suspend fun getArchiveRelation(\n        avid: Long? = null,\n        bvid: String? = null,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): BiliResponse<ArchiveRelation> {\n        checkToken(accessKey, sessData)\n        val response = client.get(\"/x/web-interface/archive/relation\") {\n            require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n            avid?.let { parameter(\"aid\", it) }\n            bvid?.let { parameter(\"bvid\", it) }\n            accessKey?.let { parameter(\"access_key\", it) }\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n        }\n        return response.body()\n    }\n    /**\n     * 为视频[avid]或[bvid]点赞或取消赞\n     *\n     * @param like 是否点赞\n     * @param csrf bili_jct\n     * @param sessData SESSDATA\n     */\n    suspend fun sendVideoLike(\n        avid: Long? = null,\n        bvid: String? = null,\n        like: Boolean = true,\n        accessKey: String? = null,\n        csrf: String? = null,\n        sessData: String? = null\n    ): Pair<Boolean, String> {\n        checkToken(accessKey, sessData)\n        val response = client.post(\"/x/web-interface/archive/like\") {\n            require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n            setBody(\n                FormDataContent(\n                    Parameters.build {\n                        avid?.let { append(\"aid\", \"$it\") }\n                        bvid?.let { append(\"bvid\", it) }\n                        append(\"like\", \"${if (like) 1 else 2}\")\n                        csrf?.let { append(\"csrf\", it) }\n                        accessKey?.let { append(\"access_key\", it) }\n                    }\n                ))\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n        }.body<BiliResponseWithoutData>()\n        return Pair(response.code == 0, response.message)\n    }\n\n    /**\n     * 检查视频[avid]或[bvid]是否已点赞\n     */\n    suspend fun checkVideoLiked(\n        avid: Long? = null,\n        bvid: String? = null,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): Boolean {\n        checkToken(accessKey, sessData)\n        val response = client.get(\"/x/web-interface/archive/has/like\") {\n            require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n            avid?.let { parameter(\"aid\", it) }\n            bvid?.let { parameter(\"bvid\", it) }\n            accessKey?.let { parameter(\"access_key\", it) }\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n        }\n        return runCatching {\n            json.decodeFromString<BiliResponse<Int>>(response.bodyAsText()).getResponseData() == 1\n\n            /*\n                response.body<BiliResponse<Int>>()会找不到序列化器而报错\n                需要在初始化Json时显示注册序列化器，下面是注册的代码\n                引入依赖\n                import kotlinx.serialization.builtins.serializer\n                import kotlinx.serialization.modules.SerializersModule\n                import kotlinx.serialization.modules.contextual\n                给Json增加serializersModule：\n                Json {\n                    serializersModule = SerializersModule {\n                        // register serializer for BiliResponse<Int> directly using reified contextual API\n                        contextual<BiliResponse<Int>>(BiliResponse.serializer(Int.serializer()))\n                    }\n                }\n            */\n            // response.body<BiliResponse<Int>>().getResponseData() == 1\n        }.getOrDefault(false)\n    }\n\n    /**\n     * 为视频[avid]或[bvid]点赞或取消赞\n     *\n     * @param like 是否顺便点赞\n     * @param multiply 投币数量\n     * @param csrf bili_jct\n     * @param sessData SESSDATA\n     */\n    suspend fun sendVideoCoin(\n        avid: Long? = null,\n        bvid: String? = null,\n        multiply: Int = 1,\n        like: Boolean = false,\n        accessKey: String? = null,\n        csrf: String? = null,\n        sessData: String? = null,\n        buvid3: String? = null\n    ): Pair<Boolean, String> {\n        checkToken(accessKey, sessData)\n        require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n//        val response = client.post(\"/x/web-interface/coin/add\") {\n        val response = client.post(\"https://app.bilibili.com/x/v2/view/coin/add\") {\n            setBody(FormDataContent(\n                Parameters.build {\n                    avid?.let { append(\"aid\", \"$it\") }\n                    bvid?.let { append(\"bvid\", it) }\n                    append(\"multiply\", \"$multiply\")\n                    append(\"select_like\", \"${if (like) 1 else 0}\")\n//                    csrf?.let { append(\"csrf\", it) }\n                    accessKey?.let { append(\"access_key\", it) }\n                }\n            ))\n//            if (sessData != null && buvid3 != null) {\n//                header(\"Cookie\", \"SESSDATA=$sessData;buvid3=$buvid3\")\n//            }\n        }.body<BiliResponse<AddCoin>>()\n        return Pair(response.code == 0, response.message)\n    }\n\n    /**\n     * 检查视频[avid]或[bvid]是否已投币\n     */\n    suspend fun checkVideoSentCoin(\n        avid: Long? = null,\n        bvid: String? = null,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): Boolean {\n        checkToken(accessKey, sessData)\n        val response = client.get(\"/x/web-interface/archive/coins\") {\n            require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n            avid?.let { parameter(\"aid\", it) }\n            bvid?.let { parameter(\"bvid\", it) }\n            accessKey?.let { parameter(\"access_key\", it) }\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n        }.body<BiliResponse<CheckSentCoin>>()\n        return runCatching {\n            response.getResponseData().multiply != 0\n        }.getOrDefault(false)\n    }\n\n    /**\n     * 为视频[avid]添加到[addMediaIds]或从[delMediaIds]移除\n     */\n    suspend fun setVideoToFavorite(\n        avid: Long,\n        type: Int = 2,\n        addMediaIds: List<Long> = listOf(),\n        delMediaIds: List<Long> = listOf(),\n        accessKey: String? = null,\n        csrf: String? = null,\n        sessData: String? = null\n    ) {\n        checkToken(accessKey, sessData)\n        val response = client.post(\"/x/v3/fav/resource/deal\") {\n            require(addMediaIds.isNotEmpty() || delMediaIds.isNotEmpty()) {\n                \"addMediaIds and delMediaIds cannot be empty at the same time\"\n            }\n            setBody(\n                FormDataContent(\n                    Parameters.build {\n                        append(\"rid\", \"$avid\")\n                        append(\"type\", \"$type\")\n                        append(\"add_media_ids\", addMediaIds.joinToString(separator = \",\"))\n                        append(\"del_media_ids\", delMediaIds.joinToString(separator = \",\"))\n                        csrf?.let { append(\"csrf\", it) }\n                        accessKey?.let { append(\"access_key\", it) }\n                    }\n                ))\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n        }.body<BiliResponse<SetVideoFavorite>>()\n        check(response.code == 0) { response.message }\n    }\n\n    /**\n     * 检查视频[avid]是否已收藏\n     */\n    suspend fun checkVideoFavoured(\n        avid: Long,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): Boolean {\n        checkToken(accessKey, sessData)\n        val response = client.get(\"/x/v2/fav/video/favoured\") {\n            parameter(\"aid\", avid)\n            accessKey?.let { parameter(\"access_key\", it) }\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n        }.body<BiliResponse<CheckVideoFavoured>>()\n        return runCatching {\n            response.getResponseData().favoured\n        }.getOrDefault(false)\n    }\n\n    /**\n     * 获取用户[mid]投稿视频\n     *\n     * @param order 排序方式 默认为pubdate 最新发布：pubdate 最多播放：click 最多收藏：stow\n     * @param tid 筛选目标分区 默认为0 0：不进行分区筛选 分区tid为所筛选的分区\n     * @param keyword 关键词筛选 用于使用关键词搜索该UP主视频稿件\n     * @param pageNumber 页码\n     * @param pageSize 每页项数 最小1，最大50\n     */\n    suspend fun getWebUserSpaceVideos(\n        mid: Long,\n        order: String = \"pubdate\",\n        tid: Int = 0,\n        keyword: String? = null,\n        pageNumber: Int = 1,\n        pageSize: Int = 30,\n        sessData: String,\n        dedeUserID: Long? = null\n    ): BiliResponse<WebSpaceVideoData> = client.get(\"/x/space/wbi/arc/search\") {\n        parameter(\"mid\", mid)\n        parameter(\"order\", order)\n        parameter(\"tid\", tid)\n        keyword?.let { parameter(\"keyword\", it) }\n        parameter(\"pn\", pageNumber)\n        parameter(\"ps\", pageSize)\n        // 风控\n        parameter(\"dm_img_list\", \"[]\")\n        parameter(\"dm_img_str\", \"V2ViR0wgMS4wIChPcGVuR0wgRVMgMi4wIENocm9taXVtKQ\")\n        parameter(\"dm_cover_img_str\", \"QU5HTEUgKEFNRCwgQU1EIFJhZGVvbiA3ODBNIEdyYXBoaWNzICgweDAwMDAxNUJGKSBEaXJlY3QzRDExIHZzXzVfMCBwc181XzAsIEQzRDExKUdvb2dsZSBJbmMuIChBTU\")\n        parameter(\"dm_img_inter\", \"{\\\"ds\\\":[],\\\"wh\\\":[4769,2793,43],\\\"of\\\":[285,570,285]}\")\n        header(\"Cookie\", \"SESSDATA=$sessData;DedeUserID=$dedeUserID;\")\n        header(\"referer\", \"https://space.bilibili.com\")\n    }.body()\n\n    suspend fun getAppUserSpaceVideos(\n        mid: Long,\n        lastAvid: Long,\n        order: String = \"pubdate\",\n        ts: Long,\n        accessKey: String\n    ): BiliResponse<AppSpaceVideoData> =\n        client.get(\"https://app.bilibili.com/x/v2/space/archive/cursor\") {\n            parameter(\"vmid\", mid)\n            parameter(\"aid\", lastAvid)\n            parameter(\"order\", order)\n            parameter(\"ts\", ts)\n            parameter(\"access_key\", accessKey)\n        }.body()\n\n    /**\n     * 获取剧集[seasonId]或[epId]的详细信息 (Web)，例如 ss24439 ep234533，传参仅需数字\n     */\n    suspend fun getWebSeasonInfo(\n        seasonId: Int? = null,\n        epId: Int? = null,\n        sessData: String = \"\"\n    ): BiliResponse<WebSeasonData> = client.get(\"/pgc/view/web/season\") {\n        require(seasonId != null || epId != null) { \"seasonId and epId cannot be null at the same time\" }\n        seasonId?.let { parameter(\"season_id\", it) }\n        epId?.let { parameter(\"ep_id\", it) }\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n        //必须得加上 referer 才能通过账号身份验证\n        header(\"referer\", \"https://www.bilibili.com\")\n    }.body()\n\n    /**\n     * 获取剧集[seasonId]或[epId]的详细信息 (App)，例如 ss24439 ep234533，传参仅需数字\n     */\n    suspend fun getAppSeasonInfo(\n        seasonId: Int? = null,\n        epId: Int? = null,\n        mobiApp: String,\n        adExtra: String? = null,\n        autoPlay: Int? = null,\n        build: Int? = null,\n        cLocale: String? = null,\n        channel: String? = null,\n        disableRcmd: Int? = null,\n        fromAv: String? = null,\n        fromSpmid: String? = null,\n        isShowAllSeries: Int? = null,\n        platform: String? = null,\n        sLocale: String? = null,\n        spmid: String? = null,\n        statistics: String? = null,\n        trackPath: String? = null,\n        trackid: String? = null,\n        ts: Int? = null,\n        accessKey: String? = \"\"\n    ): BiliResponse<AppSeasonData> = client.get(\"/pgc/view/v2/app/season\") {\n        require(seasonId != null || epId != null) { \"seasonId and epId cannot be null at the same time\" }\n        seasonId?.let { parameter(\"season_id\", it) }\n        epId?.let { parameter(\"ep_id\", it) }\n        parameter(\"mobi_app\", mobiApp)\n        adExtra?.let { parameter(\"ad_extra\", it) }\n        autoPlay?.let { parameter(\"auto_play\", it) }\n        build?.let { parameter(\"build\", it) }\n        cLocale?.let { parameter(\"c_locale\", it) }\n        channel?.let { parameter(\"channel\", it) }\n        disableRcmd?.let { parameter(\"disable_rcmd\", it) }\n        fromAv?.let { parameter(\"from_av\", it) }\n        fromSpmid?.let { parameter(\"from_spmid\", it) }\n        isShowAllSeries?.let { parameter(\"is_show_all_series\", it) }\n        platform?.let { parameter(\"platform\", it) }\n        sLocale?.let { parameter(\"s_locale\", it) }\n        spmid?.let { parameter(\"spmid\", it) }\n        statistics?.let { parameter(\"statistics\", it) }\n        trackPath?.let { parameter(\"track_path\", it) }\n        trackid?.let { parameter(\"trackid\", it) }\n        ts?.let { parameter(\"ts\", it) }\n        accessKey?.let { parameter(\"access_key\", accessKey) }\n    }.body()\n\n    /**\n     * 添加番剧[seasonId]的追番\n     */\n    suspend fun addSeasonFollow(\n        seasonId: Int,\n        csrf: String,\n        sessData: String\n    ): BiliResponse<SeasonFollowData> = client.post(\"/pgc/web/follow/add\") {\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    append(\"season_id\", \"$seasonId\")\n                    append(\"csrf\", csrf)\n                }\n            ))\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n        //必须得加上 referer 才能通过账号身份验证\n        header(\"referer\", \"https://www.bilibili.com\")\n    }.body()\n\n    /**\n     * 添加番剧[seasonId]的追番\n     */\n    suspend fun addSeasonFollow(\n        seasonId: Int,\n        accessKey: String\n    ): BiliResponse<SeasonFollowData> = client.post(\"/pgc/app/follow/add\") {\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    append(\"season_id\", \"$seasonId\")\n                    append(\"access_key\", accessKey)\n                }\n            ))\n    }.body()\n\n    /**\n     * 取消番剧[seasonId]的追番\n     */\n    suspend fun delSeasonFollow(\n        seasonId: Int,\n        csrf: String,\n        sessData: String\n    ): BiliResponse<SeasonFollowData> = client.post(\"/pgc/web/follow/del\") {\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    append(\"season_id\", \"$seasonId\")\n                    append(\"csrf\", csrf)\n                }\n            ))\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n        //必须得加上 referer 才能通过账号身份验证\n        header(\"referer\", \"https://www.bilibili.com\")\n    }.body()\n\n    /**\n     * 取消番剧[seasonId]的追番\n     */\n    suspend fun delSeasonFollow(\n        seasonId: Int,\n        accessKey: String\n    ): BiliResponse<SeasonFollowData> = client.post(\"/pgc/app/follow/del\") {\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    append(\"season_id\", \"$seasonId\")\n                    append(\"access_key\", accessKey)\n                }\n            ))\n    }.body()\n\n    /**\n     * 单独获取剧集[seasonId]的用户信息[WebSeasonData.UserStatus]\n     */\n    suspend fun getSeasonUserStatus(\n        seasonId: Int,\n        sessData: String\n    ): BiliResponse<WebSeasonData.UserStatus> = client.get(\"/pgc/view/web/season/user/status\") {\n        parameter(\"season_id\", seasonId)\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n        //必须得加上 referer 才能通过账号身份验证\n        header(\"referer\", \"https://www.bilibili.com\")\n    }.body()\n\n    /**\n     * 获取视频[avid]/[bvid]的视频标签[Tag]\n     */\n    suspend fun getVideoTags(\n        avid: Long? = null,\n        bvid: String? = null,\n        sessData: String = \"\"\n    ): BiliResponse<List<Tag>> = client.get(\"/x/tag/archive/tags\") {\n        require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n        avid?.let { parameter(\"aid\", it) }\n        bvid?.let { parameter(\"bvid\", it) }\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 获取视频标签[tagId]的详细信息，包含相关标签和最新视频\n     */\n    suspend fun getTagDetail(\n        tagId: Int,\n        pageNumber: Int,\n        pageSize: Int\n    ): BiliResponse<TagDetail> = client.get(\"/x/tag/detail\") {\n        parameter(\"tag_id\", tagId)\n        parameter(\"pn\", pageNumber)\n        parameter(\"ps\", pageSize)\n    }.body()\n\n    /**\n     * 获取视频标签[tagId]的最热门的视频列表\n     */\n    suspend fun getTagTopVideos(\n        tagId: Int,\n        pageNumber: Int,\n        pageSize: Int\n    ): TagTopVideosResponse = client.get(\"/x/web-interface/tag/top\") {\n        parameter(\"tid\", tagId)\n        parameter(\"pn\", pageNumber)\n        parameter(\"ps\", pageSize)\n    }.body()\n\n    /**\n     * 获取剧集更新时间表\n     *\n     * @param type 番剧: 1 影视（貌似只有少数几个纪录片）: 3, 国创: 4\n     */\n    suspend fun getTimeline(\n        type: Int,\n        before: Int,\n        after: Int\n    ): BiliResponse<List<Timeline>> {\n        val response = client.get(\"/pgc/web/timeline\") {\n            require(before in 0..7) { \"before must in [0,7]\" }\n            require(after in 0..7) { \"after must in [0,7]\" }\n            parameter(\"types\", type)\n            parameter(\"before\", before)\n            parameter(\"after\", after)\n        }\n        return runCatching {\n            json.decodeFromString<BiliResponse<List<Timeline>>>(response.bodyAsText())\n        }.getOrNull() ?: throw IllegalStateException(\"parse timeline data failed\")\n    }\n\n    /**\n     * 获取剧集更新时间表\n     *\n     * @param filterType 全部: 0 番剧: 1 我的追番: 2 国创: 3\n     */\n    suspend fun getTimeline(\n        filterType: Int,\n    ): BiliResponse<TimelineAppData> = client.get(\"/pgc/app/timeline\") {\n        parameter(\"filter_type\", filterType)\n        parameter(\"access_key\", \"\")\n    }.body()\n\n    /**\n     * 获取用户[mid]的关注列表，对于其他用户只能访问前5页\n     */\n    suspend fun getUserFollow(\n        mid: Long,\n        orderType: String? = null,\n        pageSize: Int = 50,\n        pageNumber: Int = 1,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): BiliResponse<UserFollowData> = client.get(\"/x/relation/followings\") {\n        checkToken(accessKey, sessData)\n        parameter(\"vmid\", mid)\n        orderType?.let { parameter(\"order_type\", orderType) }\n        parameter(\"ps\", pageSize)\n        parameter(\"pn\", pageNumber)\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$sessData;\") }\n        accessKey?.let { parameter(\"access_key\", accessKey) }\n    }.body()\n\n    /**\n     * 更改与用户[mid]之间的相互关系[action]\n     */\n    suspend fun modifyFollow(\n        mid: Long,\n        action: FollowAction,\n        actionSource: FollowActionSource,\n        accessKey: String? = null,\n        csrf: String? = null,\n        sessData: String? = null\n    ): BiliResponseWithoutData = client.post(\"/x/relation/modify\") {\n        checkToken(accessKey, sessData)\n        setBody(\n            FormDataContent(\n                Parameters.build {\n                    append(\"fid\", \"$mid\")\n                    append(\"act\", \"${action.id}\")\n                    append(\"re_src\", \"${actionSource.id}\")\n                    accessKey?.let { append(\"access_key\", accessKey) }\n                    csrf?.let { append(\"csrf\", csrf) }\n                }\n            ))\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$sessData;\") }\n    }.body()\n\n    /**\n     * 获取与用户[mid]的相互关系[RelationData]\n     *\n     * 有两个api，响应相同\n     * - https://api.bilibili.com/x/space/acc/relation\n     * - https://api.bilibili.com/x/web-interface/relation\n     */\n    suspend fun getRelations(\n        mid: Long,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): BiliResponse<RelationData> = client.get(\"/x/space/wbi/acc/relation\") {\n        checkToken(accessKey, sessData)\n        parameter(\"mid\", mid)\n        accessKey?.let { parameter(\"access_key\", accessKey) }\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$sessData;\") }\n    }.body()\n\n    /**\n     * 获取用户[mid]的关系统计（关注数，粉丝数，黑名单数）\n     */\n    suspend fun getRelationStat(\n        mid: Long,\n        accessKey: String? = null,\n        sessData: String? = null\n    ): BiliResponse<RelationStat> = client.get(\"x/relation/stat\") {\n        parameter(\"vmid\", mid)\n        accessKey?.let { parameter(\"access_key\", accessKey) }\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$sessData;\") }\n    }.body()\n\n    /**\n     * 获取搜索提示（Web）\n     *\n     * @param limit 返回数量\n     * @param platform 平台标识\n     */\n    suspend fun getWebSearchSquare(\n        limit: Int = 10,\n        platform: String? = null\n    ): BiliResponse<WebSearchSquareData> =\n        client.get(\"/x/web-interface/wbi/search/square\") {\n            parameter(\"limit\", limit)\n            platform?.let { parameter(\"platform\", platform) }\n        }.body()\n\n    /**\n     * 获取搜索提示（App）\n     *\n     * @param limit 返回数量，上限仅为 10\n     * @param platform 平台标识\n     */\n    suspend fun getAppSearchSquare(\n        limit: Int = 10,\n        platform: String? = null,\n        //accessKey: String = \"\"\n    ): BiliResponse<List<AppSearchSquareData>> =\n        client.get(\"https://app.bilibili.com/x/v2/search/square\") {\n            parameter(\"limit\", limit)\n            platform?.let { parameter(\"platform\", platform) }\n            parameter(\"build\", BiliAppConf.APP_BUILD_CODE)\n            //parameter(\"access_key\", accessKey)\n        }.body()\n\n    /**\n     * 获取搜索趋势（App）\n     *\n     * @param limit 返回数量\n     */\n    suspend fun getSearchTrendRank(\n        limit: Int = 10\n    ): BiliResponse<SearchTendingData> =\n        client.get(\"https://app.bilibili.com/x/v2/search/trending/ranking\") {\n            parameter(\"limit\", limit)\n            //platform?.let { parameter(\"platform\", platform) }\n            //parameter(\"build\", BiliAppConf.APP_BUILD_CODE)\n        }.body()\n\n    /**\n     * 获取搜索关键词建议\n     *\n     * 如果请求不带 [mainVer]，那返回的响应将只会包含 result，但不便于数据处理\n     *\n     * 如果请求中包含了 [highlight]，在返回的结果中 [KeywordSuggest.Result.tag] 的 name 会包含高亮的 html 标签\n     */\n    @OptIn(InternalAPI::class)\n    suspend fun getKeywordSuggest(\n        term: String,\n        mainVer: String = \"v1\",\n        highlight: String? = null,\n        buvid: String\n    ): KeywordSuggest {\n        // 需手动解析 json，因为返回的 Content-Type 为 null，会导致 Ktor 抛出异常\n        // io.ktor.client.call.NoTransformationFoundException: Expected response body of the type 'class dev.aaa1115910.biliapi.http.entity.search.KeywordSuggest (Kotlin reflection is not available)' but was 'class io.ktor.utils.io.ByteBufferChannel (Kotlin reflection is not available)'\n        // In response from `https://s.search.bilibili.com/main/suggest?term=xxx`\n        // Response status `200 `\n        // Response header `ContentType: null`\n        // Request header `Accept: application/json`\n        val responseText = client.get(\"https://s.search.bilibili.com/main/suggest\") {\n            parameter(\"term\", term)\n            parameter(\"main_ver\", mainVer)\n            highlight?.let { parameter(\"highlight\", it) }\n            parameter(\"buvid\", buvid)\n        }.readRawBytes().toString(Charsets.UTF_8)\n        val keywordSuggest = json.decodeFromString<KeywordSuggest>(responseText)\n        val result = json.decodeFromJsonElement<KeywordSuggest.Result>(keywordSuggest.result!!)\n        keywordSuggest.suggests.addAll(result.tag)\n        return keywordSuggest\n    }\n\n    /**\n     * 综合搜索与[keyword]相关的结果\n     */\n    suspend fun searchAll(\n        keyword: String,\n        page: Int = 1,\n        tid: Int? = null,\n        order: String? = null,\n        duration: Int? = null,\n        buvid3: String? = null\n    ): BiliResponse<SearchResultData> = client.get(\"/x/web-interface/wbi/search/all/v2\") {\n        parameter(\"keyword\", keyword)\n        parameter(\"page\", page)\n        tid?.let { parameter(\"tids\", it) }\n        order?.let { parameter(\"order\", it) }\n        duration?.let { parameter(\"duration\", it) }\n        header(\"Cookie\", \"buvid3=$buvid3;\")\n    }.body()\n\n    /**\n     * 分类搜索与[keyword]相关的[type]类型的相关结果\n     * 必须串行，要等前一个请求完成才能发起下一个请求，否则取不到数据\n     */\n    suspend fun searchType(\n        keyword: String,\n        type: String,\n        page: Int = 1,\n        tid: Int? = null,\n        order: String? = null,\n        duration: Int? = null,\n        sessData: String? = null,\n        buvid3: String? = null\n    ): BiliResponse<SearchResultData> {\n        val response = client.get(\"/x/web-interface/wbi/search/type\") {\n            parameter(\"keyword\", keyword)\n            parameter(\"search_type\", type)\n            parameter(\"page\", page)\n            tid?.let { parameter(\"tids\", it) }\n            order?.let { parameter(\"order\", it) }\n            duration?.let { parameter(\"duration\", it) }\n            if (sessData != null) {\n                header(\"Cookie\", \"SESSDATA=$sessData;buvid3=$buvid3;\")\n            } else {\n                header(\"Cookie\", \"buvid3=$buvid3;\")\n            }\n            header(\"referer\", \"https://search.bilibili.com/\")\n        }\n\n        return try {\n            response.body()\n        } catch (e: Exception) {\n            val responseText = response.bodyAsText()\n            println(\"searchType 序列化失败，原始响应内容: $responseText\")\n            throw e\n        }\n    }\n\n    /** 获取番剧首页数据 */\n    suspend fun getPgcWebInitialStateData(pgcType: PgcType): PgcWebInitialStateData {\n        val path = pgcType.name.lowercase()\n        val htmlDocuments = client.get(\"https://www.bilibili.com/$path\").body<Document>()\n\n        val dataScriptTagContent = htmlDocuments.body().select(\"script\").find {\n            it.html().contains(\"__INITIAL_STATE__\")\n        }?.html() ?: throw IllegalStateException(\"initial state data cannot be null\")\n        val dataJson =\n            dataScriptTagContent.split(\"__INITIAL_STATE__=\", \";(function()\")[1]\n        val initinalData = runCatching {\n            json.decodeFromString<PgcWebInitialStateData>(dataJson)\n        }.onFailure {\n            println(\"parse initial state data failed: ${it.stackTraceToString()}\")\n        }.getOrNull() ?: throw IllegalStateException(\"parse initial state data failed\")\n        return initinalData\n    }\n\n    /**\n     * 获取 PGC 猜你喜欢\n     *\n     * 返回数据的前几条内包含每小时更新的分类排行榜\n     */\n    suspend fun getPgcFeedV3(\n        name: String = \"anime\",\n        cursor: Int = 0\n    ): BiliResponse<PgcFeedV3Data> = client.get(\"/pgc/page/web/v3/feed\") {\n        parameter(\"name\", name)\n        parameter(\"coursor\", cursor)\n        skipAddBuvid3Cookie()\n    }.body()\n\n    /**\n     * 获取 PGC 猜你喜欢\n     */\n    suspend fun getPgcFeed(\n        name: String = \"movie\",\n        cursor: Int = 0\n    ): BiliResponse<PgcFeedData> {\n        val response = client.get(\"/pgc/page/web/feed\") {\n            parameter(\"name\", name)\n            parameter(\"coursor\", cursor)\n            parameter(\"new_cursor_status\", true)\n            skipAddBuvid3Cookie()\n        }\n        return response.body()\n    }\n\n\n    /**\n     * 获取用户[mid]的追剧列表\n     *\n     * @param type 追剧类型\n     * @param status 追剧状态\n     * @param pageNumber 页码\n     * @param pageSize 每页数量 [1, 30]\n     * @param mid 用户id\n     */\n    suspend fun getFollowingSeasons(\n        type: Int,\n        status: Int,\n        pageNumber: Int = 1,\n        pageSize: Int = 15,\n        mid: Long,\n        sessData: String? = \"\"\n    ): BiliResponse<FollowingSeasonWebData> = client.get(\"/x/space/bangumi/follow/list\") {\n        parameter(\"type\", type)\n        parameter(\"follow_status\", status)\n        parameter(\"pn\", pageNumber)\n        parameter(\"ps\", pageSize)\n        parameter(\"vmid\", mid)\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 获取用户的追剧列表\n     *\n     * @param type 追剧类型\n     * @param status 追剧状态\n     * @param pageNumber 页码\n     * @param pageSize 每页数量 [1, 30]\n     * @param build App build code\n     */\n    suspend fun getFollowingSeasons(\n        type: String,\n        status: Int,\n        pageNumber: Int = 1,\n        pageSize: Int = 15,\n        build: Int,\n        accessKey: String\n    ): BiliResponse<FollowingSeasonAppData> = client.get(\"/pgc/app/follow/v2/$type\") {\n        parameter(\"status\", status)\n        parameter(\"pn\", pageNumber)\n        parameter(\"ps\", pageSize)\n        parameter(\"build\", build)\n        parameter(\"access_key\", accessKey)\n    }.body()\n\n    /**\n     * 获取导航栏用户信息\n     *\n     * 内含 wbi keys\n     */\n    suspend fun getWebInterfaceNav(\n        buvid3: String? = null,\n        sessData: String = \"\"\n    ): BiliResponse<NavResponseData> =\n        client.get(\"/x/web-interface/nav\") {\n            if (buvid3 != null && sessData.isNotEmpty()) {\n                header(\"Cookie\", \"buvid3=$buvid3; SESSDATA=$sessData;\")\n            } else if (sessData.isNotEmpty()) {\n                header(\"Cookie\", \"SESSDATA=$sessData;\")\n            }\n        }.body()\n\n    /**\n     * 风控验证注册\n     *\n     * 使用 v_voucher 向B站申请 Geetest 验证参数。\n     *\n     * @param vVoucher 风控返回的 v_voucher 字符串\n     * @param sessData 用户登录凭证\n     * @param csrf bili_jct csrf token\n     */\n    suspend fun gaiaVgateRegister(\n        vVoucher: String,\n        sessData: String? = null,\n        csrf: String? = null\n    ): BiliResponse<GaiaVgateRegisterData> {\n        val response = client.post(\"/x/gaia-vgate/v1/register\") {\n            csrf?.let { parameter(\"csrf\", it) }\n            setBody(\n                FormDataContent(\n                    Parameters.build {\n                        append(\"v_voucher\", vVoucher)\n                    }\n                )\n            )\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n            header(\"referer\", \"https://www.bilibili.com\")\n        }\n        return response.body()\n    }\n\n    /**\n     * 风控验证校验\n     *\n     * 提交 Geetest 验证结果，获取 grisk_id 用于后续请求的 gaia_vtoken 参数。\n     *\n     * @param token 由 [gaiaVgateRegister] 返回的 token\n     * @param geetestChallenge Geetest challenge\n     * @param validate Geetest validate\n     * @param seccode Geetest seccode\n     * @param sessData 用户登录凭证\n     * @param csrf bili_jct csrf token\n     */\n    suspend fun gaiaVgateValidate(\n        token: String,\n        geetestChallenge: String,\n        validate: String,\n        seccode: String,\n        sessData: String? = null,\n        csrf: String? = null\n    ): BiliResponse<GaiaVgateValidateData> {\n        val response = client.post(\"/x/gaia-vgate/v1/validate\") {\n            csrf?.let { parameter(\"csrf\", it) }\n            setBody(\n                FormDataContent(\n                    Parameters.build {\n                        append(\"token\", token)\n                        append(\"challenge\", geetestChallenge)\n                        append(\"validate\", validate)\n                        append(\"seccode\", seccode)\n                    }\n                )\n            )\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n            header(\"referer\", \"https://www.bilibili.com\")\n        }\n        return response.body()\n    }\n\n    /**\n     * 更新 wbi keys\n     * @param sessData 用户登录凭证，默认使用 sessDataProvider 获取\n     * @param buvid3 设备标识，默认使用 buvid3Provider 获取\n     */\n    suspend fun updateWbi(\n        sessData: String = sessDataProvider(),\n        buvid3: String? = buvid3Provider()\n    ) {\n        val needToUpdate =\n            wbiImgKey == null || wbiSubKey == null || System.currentTimeMillis() - wbiLastRefreshDate < 2 * 60 * 60 * 1000L\n        if (!needToUpdate) {\n            println(\"Skip update wbi keys\")\n            return\n        }\n\n        println(\"Updating wbi keys...\")\n        runCatching {\n            val wbiData = getWebInterfaceNav(buvid3 = buvid3, sessData = sessData).data!!.wbiImg\n            wbiImgKey = wbiData.getImgKey()\n            wbiSubKey = wbiData.getSubKey()\n            wbiLastRefreshDate = System.currentTimeMillis()\n        }.onSuccess {\n            println(\"Update wbi data success\")\n        }.onFailure {\n            println(\"Update wbi data failed: ${it.stackTraceToString()}\")\n        }\n    }\n\n    /**\n     * 获取首页视频推荐列表（Web）\n     */\n    suspend fun getFeedRcmd(\n        freshType: Int = 4,\n        pageSize: Int = 30,\n        idx: Int = 1,\n        buvid3: String? = null,\n        sessData: String? = null\n    ): BiliResponse<RcmdTopData> = client.get(\"/x/web-interface/wbi/index/top/feed/rcmd\") {\n        parameter(\"fresh_type\", freshType)\n        parameter(\"ps\", pageSize)\n        parameter(\"fresh_idx\", idx)\n        parameter(\"fresh_idx_1h\", idx)\n        if (sessData != null && buvid3 != null) {\n            header(\"Cookie\", \"buvid3=$buvid3; SESSDATA=$sessData;\")\n        } else {\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n        }\n    }.body()\n\n    /**\n     * 获取首页视频推荐列表（App）\n     */\n    suspend fun getFeedIndex(\n        idx: Int = 0,\n        accessKey: String? = null,\n    ): BiliResponse<RcmdIndexData> =\n        client.get(\"https://app.bilibili.com/x/v2/feed/index\") {\n            parameter(\"idx\", idx)\n            accessKey?.let { parameter(\"access_key\", it) }\n        }.body()\n\n    private suspend fun seasonIndexResult(\n        seasonIndexType: SeasonIndexType,\n        order: Int? = null,\n        seasonVersion: Int? = null,\n        spokenLanguageType: Int? = null,\n        area: Int? = null,\n        isFinish: Int? = null,\n        copyright: Int? = null,\n        seasonStatus: Int? = null,\n        seasonMonth: Int? = null,\n        year: String? = null,\n        releaseDate: String? = null,\n        styleId: Int? = null,\n        producerId: Int? = null,\n        sort: Int? = null,\n        page: Int? = null,\n        pagesize: Int? = null,\n        type: Int? = null\n    ): BiliResponse<IndexResultData> = client.get(\"/pgc/season/index/result\") {\n        parameter(\"st\", seasonIndexType.id)\n        order?.let { parameter(\"order\", it) }\n        seasonVersion?.let { parameter(\"season_version\", it) }\n        spokenLanguageType?.let { parameter(\"spoken_language_type\", it) }\n        area?.let { parameter(\"area\", it) }\n        isFinish?.let { parameter(\"is_finish\", it) }\n        copyright?.let { parameter(\"copyright\", it) }\n        seasonStatus?.let { parameter(\"season_status\", it) }\n        seasonMonth?.let { parameter(\"season_month\", it) }\n        year?.let { parameter(\"year\", it) }\n        releaseDate?.let { parameter(\"release_date\", it) }\n        styleId?.let { parameter(\"style_id\", it) }\n        producerId?.let { parameter(\"producer_id\", it) }\n        sort?.let { parameter(\"sort\", it) }\n        page?.let { parameter(\"page\", it) }\n        parameter(\"season_type\", seasonIndexType.id)\n        pagesize?.let { parameter(\"pagesize\", it) }\n        type?.let { parameter(\"type\", it) }\n    }.body()\n\n    suspend fun seasonIndexCondition(\n        seasonIndexType: SeasonIndexType,\n        type: Int = 0\n    ): BiliResponse<PgcIndexConditionData> = client.get(\"/pgc/season/index/condition\") {\n        parameter(\"season_type\", seasonIndexType.id)\n        parameter(\"type\", type)\n    }.body()\n\n    suspend fun seasonIndexDynamicResult(\n        seasonIndexType: SeasonIndexType,\n        order: String,\n        sort: String,\n        filters: Map<String, String>,\n        page: Int = 1,\n        pagesize: Int = 20,\n        type: Int = 0\n    ): BiliResponse<IndexResultData> = client.get(\"/pgc/season/index/result\") {\n        parameter(\"st\", seasonIndexType.id)\n        parameter(\"order\", order)\n        parameter(\"sort\", sort)\n        filters.forEach { (field, keyword) ->\n            parameter(field, keyword)\n        }\n        parameter(\"season_type\", seasonIndexType.id)\n        parameter(\"page\", page)\n        parameter(\"pagesize\", pagesize)\n        parameter(\"type\", type)\n    }.body()\n\n    suspend fun seasonIndexAnimeResult(\n        order: Int = 0,\n        seasonVersion: Int = -1,\n        spokenLanguageType: Int = -1,\n        area: Int = -1,\n        isFinish: Int = -1,\n        copyright: Int = -1,\n        seasonStatus: Int = -1,\n        seasonMonth: Int = -1,\n        year: String = \"-1\",\n        styleId: Int = -1,\n        sort: Int = 0,\n        page: Int = 1,\n        pagesize: Int = 20,\n        type: Int = 1\n    ) = seasonIndexResult(\n        seasonIndexType = SeasonIndexType.Anime,\n        order = order,\n        seasonVersion = seasonVersion,\n        spokenLanguageType = spokenLanguageType,\n        area = area,\n        isFinish = isFinish,\n        copyright = copyright,\n        seasonStatus = seasonStatus,\n        seasonMonth = seasonMonth,\n        year = year,\n        styleId = styleId,\n        sort = sort,\n        page = page,\n        pagesize = pagesize,\n        type = type\n    )\n\n    suspend fun seasonIndexGuochuangResult(\n        order: Int = 0,\n        seasonVersion: Int = -1,\n        isFinish: Int = -1,\n        copyright: Int = -1,\n        seasonStatus: Int = -1,\n        year: String = \"-1\",\n        styleId: Int = -1,\n        sort: Int = 0,\n        page: Int = 1,\n        pagesize: Int = 20,\n        type: Int = 1\n    ) = seasonIndexResult(\n        seasonIndexType = SeasonIndexType.Guochuang,\n        order = order,\n        seasonVersion = seasonVersion,\n        isFinish = isFinish,\n        copyright = copyright,\n        seasonStatus = seasonStatus,\n        year = year,\n        styleId = styleId,\n        sort = sort,\n        page = page,\n        pagesize = pagesize,\n        type = type\n    )\n\n    suspend fun seasonIndexVarietyResult(\n        order: Int = 0,\n        seasonStatus: Int = -1,\n        styleId: Int = -1,\n        sort: Int = 0,\n        page: Int = 1,\n        pagesize: Int = 20,\n        type: Int = 1\n    ) = seasonIndexResult(\n        seasonIndexType = SeasonIndexType.Variety,\n        order = order,\n        seasonStatus = seasonStatus,\n        styleId = styleId,\n        sort = sort,\n        page = page,\n        pagesize = pagesize,\n        type = type\n    )\n\n    suspend fun seasonIndexMovieResult(\n        order: Int = 0,\n        area: Int = -1,\n        styleId: Int = -1,\n        releaseDate: String = \"-1\",\n        seasonStatus: Int = -1,\n        sort: Int = 0,\n        page: Int = 1,\n        pagesize: Int = 20,\n        type: Int = 1\n    ) = seasonIndexResult(\n        seasonIndexType = SeasonIndexType.Movie,\n        order = order,\n        area = area,\n        styleId = styleId,\n        releaseDate = releaseDate,\n        seasonStatus = seasonStatus,\n        sort = sort,\n        page = page,\n        pagesize = pagesize,\n        type = type\n    )\n\n    suspend fun seasonIndexTvResult(\n        order: Int = 0,\n        area: Int = -1,\n        styleId: Int = -1,\n        releaseDate: String = \"-1\",\n        seasonStatus: Int = -1,\n        sort: Int = 0,\n        page: Int = 1,\n        pagesize: Int = 20,\n        type: Int = 1\n    ) = seasonIndexResult(\n        seasonIndexType = SeasonIndexType.Tv,\n        order = order,\n        area = area,\n        styleId = styleId,\n        releaseDate = releaseDate,\n        seasonStatus = seasonStatus,\n        sort = sort,\n        page = page,\n        pagesize = pagesize,\n        type = type\n    )\n\n    suspend fun seasonIndexDocumentaryResult(\n        order: Int = 0,\n        area: Int = -1,\n        styleId: Int = -1,\n        producerId: Int = -1,\n        releaseDate: String = \"-1\",\n        seasonStatus: Int = -1,\n        sort: Int = 0,\n        page: Int = 1,\n        pagesize: Int = 20,\n        type: Int = 1\n    ) = seasonIndexResult(\n        seasonIndexType = SeasonIndexType.Documentary,\n        order = order,\n        area = area,\n        styleId = styleId,\n        producerId = producerId,\n        releaseDate = releaseDate,\n        seasonStatus = seasonStatus,\n        sort = sort,\n        page = page,\n        pagesize = pagesize,\n        type = type\n    )\n\n    suspend fun download(url: String): ByteArray {\n        return client.get(url).readRawBytes()\n    }\n\n    suspend fun getWebVideoShot(\n        aid: Long? = null,\n        bvid: String? = null,\n        cid: Long? = null,\n        needJsonArrayIndex: Boolean = false\n    ): BiliResponse<VideoShot> = client.get(\"/x/player/videoshot\") {\n        require(aid != null || bvid != null) { \"av and bv cannot be null at the same time\" }\n        aid?.let { parameter(\"aid\", it) }\n        bvid?.let { parameter(\"bvid\", it) }\n        cid?.let { parameter(\"cid\", it) }\n        parameter(\"index\", if (needJsonArrayIndex) 1 else 0)\n    }.body()\n\n    suspend fun getAppVideoShot(\n        aid: Long,\n        cid: Long\n    ): BiliResponse<VideoShot> = client.get(\"https://app.bilibili.com/x/v2/view/video/shot\") {\n        parameter(\"aid\", aid)\n        parameter(\"cid\", cid)\n        parameter(\"ts\", 0)\n    }.body()\n\n    suspend fun getUserEquippedGarb(\n        part: EquipPart,\n        sessData: String\n    ): BiliResponse<Equip> = client.get(\"/x/garb/user/equip\") {\n        parameter(\"part\", part.value)\n        header(\"Cookie\", \"SESSDATA=$sessData;\")\n    }.body()\n\n    /**\n     * 获取分区动态（App），包含顶部轮播图，大卡片活动推广位，和视频列表第一页\n     */\n    suspend fun getRegionDynamic(\n        rid: Int,\n        accessKey: String\n    ): BiliResponse<RegionDynamic> = client.get(\"https://app.bilibili.com/x/v2/region/dynamic\") {\n        parameter(\"access_key\", accessKey)\n        parameter(\"build\", BiliAppConf.APP_BUILD_CODE)\n        parameter(\"rid\", rid)\n    }.body()\n\n    /**\n     * 获取分区视频列表（App）,用于[getRegionDynamic]加载数据后下滑加载更多数据\n     */\n    suspend fun getRegionDynamicList(\n        rid: Int,\n        ctime: Long = 0,\n        accessKey: String\n    ): BiliResponse<RegionDynamicList> =\n        client.get(\"https://app.bilibili.com/x/v2/region/dynamic/list\") {\n            parameter(\"access_key\", accessKey)\n            parameter(\"build\", BiliAppConf.APP_BUILD_CODE)\n            parameter(\"rid\", rid)\n            parameter(\"ctime\", ctime)\n            parameter(\"pull\", \"false\")\n        }.body()\n\n    //\n\n    /**\n     * 获取分区内各种插入的banner，例如顶部轮播图，还有插入的广告横幅（Web）\n     *\n     * id:\n     * 4973  动画  douga\n     * 4991  游戏  game\n     * 5004  鬼畜  kichiku\n     * 4979  音乐  music\n     * 4985  舞蹈  dance\n     * 5008  影视  cinephile\n     * 5007  娱乐  ent\n     * 4997  知识  knowledge\n     * 4998  科技  tech\n     * 5005  资讯  information\n     * 5002  美食  food\n     * 5001  生活  life\n     * 5000  汽车  car\n     * 5006  时尚  fashion\n     * 4999  运动  sports\n     * 5003  动物圈 animal\n     */\n    suspend fun getLocs(\n        ids: List<Int>,\n        sessData: String? = null\n    ): RegionLocs = client.get(\"/x/web-show/res/locs\") {\n        parameter(\"ids\", ids.joinToString(\",\"))\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n    }.body()\n\n    /**\n     * 获取评论\n     *\n     * @param type 评论类型\n     * @param oid 评论区id\n     * @param mode 评论排序方式 默认为 3， 0 3：仅按热度 1：按热度+按时间 2：仅按时间\n     * @param paginationStr 分页参数\n     */\n    suspend fun getComments(\n        type: Long,\n        oid: Long,\n        mode: Int = 3,\n        paginationStr: String = \"\"\"{\"offset\":\"\"}\"\"\",\n        //webLocation: Int = 1815875,\n        sessData: String? = null,\n        dedeUserID: Long? = null,\n        buvid3: String? = null\n    ): BiliResponse<CommentData> =\n        client.get(\"/x/v2/reply/wbi/main\") {\n            parameter(\"type\", type)\n            parameter(\"oid\", oid)\n            parameter(\"mode\", mode)\n            parameter(\"pagination_str\", paginationStr)\n            //parameter(\"web_location\", webLocation)\n\n            val cookieParts = mutableListOf<String>()\n            sessData?.takeIf { it.isNotBlank() }?.let { cookieParts.add(\"SESSDATA=$it\") }\n            dedeUserID?.let { cookieParts.add(\"DedeUserID=$it\") }\n            buvid3?.takeIf { it.isNotBlank() }?.let { cookieParts.add(\"buvid3=$it\") }\n            if (cookieParts.isNotEmpty()) {\n                header(\"Cookie\", cookieParts.joinToString(\";\") + \";\")\n            }\n        }.body()\n\n    suspend fun getCommentReplies(\n        oid: Long,\n        type: Long,\n        root: Long,\n        pageSize: Int = 20,\n        pageNumber: Int = 1,\n        sessData: String? = null,\n        dedeUserID: Long? = null,\n        buvid3: String? = null\n    ): BiliResponse<CommentReplyData> {\n        var response = client.get(\"/x/v2/reply/reply\") {\n            parameter(\"oid\", oid)\n            parameter(\"type\", type)\n            parameter(\"root\", root)\n            parameter(\"ps\", pageSize)\n            parameter(\"pn\", pageNumber)\n\n            val cookieParts = mutableListOf<String>()\n            sessData?.takeIf { it.isNotBlank() }?.let { cookieParts.add(\"SESSDATA=$it\") }\n            dedeUserID?.let { cookieParts.add(\"DedeUserID=$it\") }\n            buvid3?.takeIf { it.isNotBlank() }?.let { cookieParts.add(\"buvid3=$it\") }\n            if (cookieParts.isNotEmpty()) {\n                header(\"Cookie\", cookieParts.joinToString(\";\") + \";\")\n            }\n        }\n        // println(response.bodyAsText())\n        return response.body()\n    }\n\n    suspend fun getSeasonIdByAvid(\n        avid: Long\n    ): Int? {\n        return runCatching {\n            val data = getPgcVideoPlayUrlV2(av = avid).getResponseData()\n            data.playViewBusinessInfo.seasonInfo.seasonId\n        }.getOrNull()\n    }\n\n    suspend fun getAidCidByEpid(\n        epid: Int\n    ): Pair<Long, Long>? {\n        return runCatching {\n            val data = getPgcVideoPlayUrlV2(epid = epid).getResponseData()\n            data.playViewBusinessInfo.episodeInfo.aid to data.playViewBusinessInfo.episodeInfo.cid\n        }.getOrNull()\n    }\n\n    /**\n     * 获取 UGC 分区轮播图\n     */\n    suspend fun getRegionBanner(\n        regionId: Int\n    ): BiliResponse<RegionBanner> = client.get(\"/x/web-show/region/banner\") {\n        parameter(\"region_id\", regionId)\n    }.body()\n\n    /**\n     * 获取 UGC 分区推荐视频\n     *\n     * @param displayId 页数\n     * @param requestCnt 每页数量\n     * @param fromRegion 分区id\n     */\n    suspend fun getRegionFeedRcmd(\n        displayId: Int,\n        requestCnt: Int = 15,\n        fromRegion: Int,\n        device: String = \"web\",\n        plat: Int = 30,\n        sessData: String? = null\n    ): BiliResponse<RegionFeedRcmd> = client.get(\"/x/web-interface/region/feed/rcmd\") {\n        parameter(\"display_id\", displayId)\n        parameter(\"request_cnt\", requestCnt)\n        parameter(\"from_region\", fromRegion)\n        parameter(\"device\", device)\n        parameter(\"plat\", plat)\n        sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n    }.body()\n\n    /**\n     * 一键三连\n     */\n    suspend fun tripleLike(\n        avid: Long? = null,\n        bvid: String? = null,\n        csrf: String? = null,\n        sessData: String? = null,\n        accessKey: String? = null\n    ): Pair<Boolean, String> {\n        checkToken(accessKey, sessData)\n        require(avid != null || bvid != null) { \"avid and bvid cannot be null at the same time\" }\n\n        // 使用 App API（当只有 accessKey 时）\n        val useAppApi = accessKey != null && sessData == null\n        val url = if (useAppApi) {\n            \"https://app.bilibili.com/x/v2/view/like/triple\"\n        } else {\n            \"/x/web-interface/archive/like/triple\"\n        }\n\n        val response = client.post(url) {\n            setBody(\n                FormDataContent(\n                    Parameters.build {\n                        avid?.let { append(\"aid\", \"$it\") }\n                        bvid?.let { append(\"bvid\", it) }\n                        if (!useAppApi) {\n                            csrf?.let { append(\"csrf\", it) }\n                        }\n                        accessKey?.let { append(\"access_key\", it) }\n                    }\n                ))\n            sessData?.let { header(\"Cookie\", \"SESSDATA=$it;\") }\n        }.body<BiliResponseWithoutData>()\n        return Pair(response.code == 0, response.message)\n    }\n}\n\nenum class SeasonIndexType(val id: Int) {\n    Anime(1), Movie(2), Documentary(3), Guochuang(4), Tv(5), Variety(7);\n\n    companion object {\n        fun fromId(id: Int) = entries.first { it.id == id }\n    }\n}\n\nprivate fun checkToken(accessKey: String?, sessData: String?) {\n    require(accessKey != null || sessData != null) { \"accessKey and sessData cannot be null at the same time\" }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/BiliHttpConstants.kt",
    "content": "package dev.aaa1115910.biliapi.http\n\nobject BiliHttpConstants {\n    const val USER_AGENT_WEB = \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36\"\n    const val USER_AGENT_APP = \"Bilibili Freedoooooom/MarkII\"\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/BiliHttpProxyApi.kt",
    "content": "package dev.aaa1115910.biliapi.http\n\nimport dev.aaa1115910.biliapi.BiliApiConstants\nimport dev.aaa1115910.biliapi.http.entity.BiliResponse\nimport dev.aaa1115910.biliapi.http.entity.search.SearchResultData\nimport dev.aaa1115910.biliapi.http.entity.video.PlayUrlData\nimport dev.aaa1115910.biliapi.http.entity.video.PlayUrlV2Data\nimport dev.aaa1115910.biliapi.http.plugins.BiliUserAgent\nimport dev.aaa1115910.biliapi.http.util.BiliDns\nimport dev.aaa1115910.biliapi.http.util.encApiSign\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.HttpRequestRetry\nimport io.ktor.client.plugins.compression.ContentEncoding\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport io.ktor.http.URLProtocol\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.json.Json\n\nobject BiliHttpProxyApi {\n    private var client: HttpClient? = null\n\n    private val json = Json {\n        coerceInputValues = true\n        ignoreUnknownKeys = true\n        prettyPrint = true\n    }\n\n    fun createClient(proxyServer: String) {\n        client = HttpClient(OkHttp) {\n            engine {\n                config {\n                    dns(BiliDns)\n                }\n            }\n            BiliUserAgent()\n            install(ContentNegotiation) {\n                json(json)\n            }\n            install(ContentEncoding) {\n                deflate(1.0F)\n                gzip(0.9F)\n            }\n            install(HttpRequestRetry) {\n                retryOnException(maxRetries = 2)\n            }\n            defaultRequest {\n                url {\n                    val proxyServerSpilt = proxyServer.split(\":\")\n                    val endPoint = proxyServerSpilt.first()\n                    val port = proxyServerSpilt.getOrNull(1)?.toInt()\n                    host = endPoint\n                    if (endPoint == \"127.0.0.1\") {\n                        //local debug\n                        this.port = 8080\n                    } else {\n                        if (port != null) {\n                            this.port = port\n                        } else {\n                            protocol = URLProtocol.HTTPS\n                        }\n                    }\n                }\n            }\n        }.apply {\n            encApiSign()\n        }\n    }\n\n    suspend fun getPgcVideoPlayUrl(\n        av: Long? = null,\n        bv: String? = null,\n        epid: Int? = null,\n        cid: Long? = null,\n        qn: Int? = null,\n        fnval: Int? = null,\n        fnver: Int? = null,\n        fourk: Int? = null,\n        session: String? = null,\n        supportMultiAudio: Boolean? = null,\n        drmTechType: Int? = null,\n        fromClient: String? = null,\n        sessData: String? = null,\n        dedeUserID: Long? = null,\n        buvid3: String? = null\n    ): BiliResponse<PlayUrlData> = client?.get(\"/pgc/player/web/playurl\") {\n        require(av != null || bv != null) { \"av and bv cannot be null at the same time\" }\n        require(epid != null || cid != null) { \"epid and cid cannot be null at the same time\" }\n        av?.let { parameter(\"avid\", it) }\n        bv?.let { parameter(\"bvid\", it) }\n        epid?.let { parameter(\"ep_id\", it) }\n        cid?.let { parameter(\"cid\", it) }\n        qn?.let { parameter(\"qn\", it) }\n        fnval?.let { parameter(\"fnval\", it) }\n        fnver?.let { parameter(\"fnver\", it) }\n        fourk?.let { parameter(\"fourk\", it) }\n        session?.let { parameter(\"session\", it) }\n        supportMultiAudio?.let { parameter(\"support_multi_audio\", it) }\n        drmTechType?.let { parameter(\"drm_tech_type\", it) }\n        fromClient?.let { parameter(\"from_client\", it) }\n        val cookieParts = mutableListOf<String>()\n        sessData?.let { cookieParts.add(\"SESSDATA=$it\") }\n        dedeUserID?.let { cookieParts.add(\"DedeUserID=$it\") }\n        buvid3?.let { cookieParts.add(\"buvid3=$it\") }\n        if (cookieParts.isNotEmpty()) header(\"Cookie\", cookieParts.joinToString(\";\"))\n        //必须得加上 referer 才能通过账号身份验证\n        header(\"referer\", \"https://www.bilibili.com\")\n    }?.body() ?: throw IllegalStateException(\"no proxy server\")\n\n    suspend fun getPgcVideoPlayUrlV2(\n        av: Long? = null,\n        bv: String? = null,\n        epid: Int? = null,\n        cid: Long? = null,\n        qn: Int? = null,\n        fnval: Int? = null,\n        fnver: Int? = null,\n        fourk: Int? = null,\n        session: String? = null,\n        supportMultiAudio: Boolean? = null,\n        drmTechType: Int? = null,\n        fromClient: String? = null,\n        sessData: String? = null,\n        buvid3: String? = null\n    ): BiliResponse<PlayUrlV2Data> = client?.get(\"/pgc/player/web/v2/playurl\") {\n        require(av != null || bv != null) { \"av and bv cannot be null at the same time\" }\n        require(epid != null || cid != null) { \"epid and cid cannot be null at the same time\" }\n        av?.let { parameter(\"avid\", it) }\n        bv?.let { parameter(\"bvid\", it) }\n        epid?.let { parameter(\"ep_id\", it) }\n        cid?.let { parameter(\"cid\", it) }\n        qn?.let { parameter(\"qn\", it) }\n        fnval?.let { parameter(\"fnval\", it) }\n        fnver?.let { parameter(\"fnver\", it) }\n        fourk?.let { parameter(\"fourk\", it) }\n        session?.let { parameter(\"session\", it) }\n        supportMultiAudio?.let { parameter(\"support_multi_audio\", it) }\n        drmTechType?.let { parameter(\"drm_tech_type\", it) }\n        fromClient?.let { parameter(\"from_client\", it) }\n        val cookieParts = mutableListOf<String>()\n        sessData?.let { cookieParts.add(\"SESSDATA=$it\") }\n        buvid3?.let { cookieParts.add(\"buvid3=$it\") }\n        if (cookieParts.isNotEmpty()) header(\"Cookie\", cookieParts.joinToString(\";\"))\n        //必须得加上 referer 才能通过账号身份验证\n        header(\"referer\", \"https://www.bilibili.com\")\n    }?.body() ?: throw IllegalStateException(\"no proxy server\")\n\n    /**\n     * 分类搜索与[keyword]相关的[type]类型的相关结果\n     */\n    suspend fun searchType(\n        keyword: String,\n        type: String,\n        page: Int = 1,\n        tid: Int? = null,\n        order: String? = null,\n        duration: Int? = null,\n        sessData: String? = null,\n        buvid3: String? = null\n    ): BiliResponse<SearchResultData> = client?.get(\"/x/web-interface/wbi/search/type\") {\n        parameter(\"keyword\", keyword)\n        parameter(\"search_type\", type)\n        parameter(\"page\", page)\n        tid?.let { parameter(\"tids\", it) }\n        order?.let { parameter(\"order\", it) }\n        duration?.let { parameter(\"duration\", it) }\n        if (sessData != null) {\n            header(\"Cookie\", \"SESSDATA=$sessData;buvid3=$buvid3;\")\n        } else {\n            header(\"Cookie\", \"buvid3=$buvid3;\")\n        }\n        header(\"referer\", \"https://search.bilibili.com/\")\n    }?.body() ?: throw IllegalStateException(\"no proxy server\")\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/BiliLiveHttpApi.kt",
    "content": "package dev.aaa1115910.biliapi.http\n\nimport dev.aaa1115910.biliapi.BiliApiConstants\nimport dev.aaa1115910.biliapi.http.entity.BiliResponse\nimport dev.aaa1115910.biliapi.http.entity.live.DanmuInfoData\nimport dev.aaa1115910.biliapi.http.entity.live.HistoryDanmaku\nimport dev.aaa1115910.biliapi.http.entity.live.RoomPlayInfoData\nimport dev.aaa1115910.biliapi.http.plugins.BiliUserAgent\nimport dev.aaa1115910.biliapi.http.util.BiliDns\nimport dev.aaa1115910.biliapi.http.util.encWbi\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.compression.ContentEncoding\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport io.ktor.http.URLProtocol\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.json.Json\n\nobject BiliLiveHttpApi {\n    private var endPoint: String = \"\"\n    private lateinit var client: HttpClient\n    private val logger = KotlinLogging.logger { }\n\n    init {\n        createClient()\n    }\n\n    private fun createClient() {\n        client = HttpClient(OkHttp) {\n            engine {\n                config {\n                    dns(BiliDns)\n                }\n            }\n            BiliUserAgent()\n            install(ContentNegotiation) {\n                json(Json {\n                    coerceInputValues = true\n                    ignoreUnknownKeys = true\n                    prettyPrint = true\n                })\n            }\n            install(ContentEncoding) {\n                deflate(1.0F)\n                gzip(0.9F)\n            }\n            defaultRequest {\n                url {\n                    host = \"api.live.bilibili.com\"\n                    protocol = URLProtocol.HTTPS\n                }\n            }\n        }\n    }\n\n    /**\n     * 获取直播间[roomId]的弹幕连接地址等信息，例如 token\n     * 需要 WBI 签名\n     * @param sessData 用户登录凭证，传入后可获取已登录用户权限的弹幕 token\n     */\n    suspend fun getLiveDanmuInfo(roomId: Int, sessData: String = \"\"): BiliResponse<DanmuInfoData> =\n        client.get(\"/xlive/web-room/v1/index/getDanmuInfo\") {\n            parameter(\"id\", roomId)\n            parameter(\"type\", 0)\n            encWbi()\n            if (sessData.isNotEmpty()) header(\"Cookie\", \"SESSDATA=$sessData\")\n        }.body()\n\n    /**\n     * 获取直播间[roomId]的信息\n     */\n    suspend fun getLiveRoomPlayInfo(roomId: Int): BiliResponse<RoomPlayInfoData> =\n        client.get(\"/xlive/web-room/v1/index/getRoomPlayInfo\") {\n            parameter(\"room_id\", roomId)\n        }.body()\n\n    /**\n     * 获取直播间[roomId]的历史弹幕\n     */\n    suspend fun getLiveDanmuHistory(roomId: Int): BiliResponse<HistoryDanmaku> =\n        client.get(\"/xlive/web-room/v1/dM/gethistory\") {\n            parameter(\"roomid\", roomId)\n        }.body()\n\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/BiliPassportHttpApi.kt",
    "content": "package dev.aaa1115910.biliapi.http\n\nimport dev.aaa1115910.biliapi.BiliApiConstants\nimport dev.aaa1115910.biliapi.http.entity.BiliResponse\nimport dev.aaa1115910.biliapi.http.entity.login.CaptchaData\nimport dev.aaa1115910.biliapi.http.entity.login.qr.AppQRDataRequest\nimport dev.aaa1115910.biliapi.http.entity.login.qr.AppQRLoginData\nimport dev.aaa1115910.biliapi.http.entity.login.qr.RequestWebQRData\nimport dev.aaa1115910.biliapi.http.entity.login.qr.WebQRLoginData\nimport dev.aaa1115910.biliapi.http.entity.login.sms.SendSmsResponse\nimport dev.aaa1115910.biliapi.http.entity.login.sms.SmsLoginResponse\nimport dev.aaa1115910.biliapi.http.plugins.BiliUserAgent\nimport dev.aaa1115910.biliapi.http.util.BiliDns\nimport dev.aaa1115910.biliapi.http.util.encApiSign\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.compression.ContentEncoding\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.request.forms.FormDataContent\nimport io.ktor.client.request.get\nimport io.ktor.client.request.parameter\nimport io.ktor.client.request.post\nimport io.ktor.client.request.setBody\nimport io.ktor.client.statement.bodyAsText\nimport io.ktor.http.Cookie\nimport io.ktor.http.Parameters\nimport io.ktor.http.URLProtocol\nimport io.ktor.http.setCookie\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.json.Json\n\nobject BiliPassportHttpApi {\n    private lateinit var client: HttpClient\n    private val json = Json {\n        coerceInputValues = true\n        ignoreUnknownKeys = true\n        prettyPrint = true\n    }\n\n    init {\n        createClient()\n    }\n\n    private fun createClient() {\n        client = HttpClient(OkHttp) {\n            engine {\n                config {\n                    dns(BiliDns)\n                }\n            }\n            BiliUserAgent()\n            install(ContentNegotiation) {\n                json(Json {\n                    coerceInputValues = true\n                    ignoreUnknownKeys = true\n                    prettyPrint = true\n                })\n            }\n            install(ContentEncoding) {\n                deflate(1.0F)\n                gzip(0.9F)\n            }\n            defaultRequest {\n                url {\n                    host = \"passport.bilibili.com\"\n                    protocol = URLProtocol.HTTPS\n                }\n            }\n        }.apply {\n            encApiSign()\n        }\n    }\n\n    /**\n     * 申请二维码（Web）\n     */\n    suspend fun getWebQRUrl(): BiliResponse<RequestWebQRData> =\n        client.get(\"/x/passport-login/web/qrcode/generate\").body()\n\n    /**\n     * 使用[qrcodeKey]进行二维码登录\n     */\n    suspend fun loginWithWebQR(qrcodeKey: String): Pair<BiliResponse<WebQRLoginData>, List<Cookie>> {\n        val loginResponse = client.get(\"/x/passport-login/web/qrcode/poll\") {\n            parameter(\"qrcode_key\", qrcodeKey)\n        }\n        return Pair(loginResponse.body(), loginResponse.setCookie())\n    }\n\n    /**\n     * 申请二维码（App）\n     */\n    suspend fun getAppQRUrl(\n        localId: String? = null,\n        ts: Int,\n        mobiApp: String? = null\n    ): BiliResponse<AppQRDataRequest> =\n        client.post(\"/x/passport-tv-login/qrcode/auth_code\") {\n            setBody(FormDataContent(\n                Parameters.build {\n                    localId?.let { append(\"local_id\", it) }\n                    append(\"ts\", \"$ts\")\n                    mobiApp?.let { append(\"mobi_app\", it) }\n                }\n            ))\n        }.body()\n\n\n    /**\n     * 使用[authCode]进行二维码登录\n     */\n    suspend fun loginWithAppQR(\n        authCode: String,\n        localId: String? = null,\n        ts: Int\n    ): BiliResponse<AppQRLoginData> =\n        client.post(\"/x/passport-tv-login/qrcode/poll\") {\n            setBody(FormDataContent(\n                Parameters.build {\n                    append(\"auth_code\", authCode)\n                    localId?.let { append(\"local_id\", it) }\n                    append(\"ts\", \"$ts\")\n                }\n            ))\n        }.body()\n\n    /**\n     * 申请 captcha 验证码\n     *\n     * @param source 获取来源 已知：main_web\n     */\n    suspend fun getCaptcha(\n        source: String? = null\n    ): BiliResponse<CaptchaData> =\n        client.get(\"/x/passport-login/captcha\") {\n            source?.let { parameter(\"source\", it) }\n        }.body()\n\n    /**\n     * 发送短信验证码\n     *\n     * @param cid 国际冠字码\n     * @param tel 手机号码\n     * @param loginSessionId 登录标识 uuid去掉'-'后得到\n     * @param channel 一般固定值为\"bili\"\n     * @param buvid\n     * @param statistics 一般固定为{\"appId\":1,\"platform\":3,\"version\":\"7.27.0\",\"abtest\":\"\"}\n     */\n    suspend fun sendSms(\n        cid: Long,\n        tel: Long,\n        loginSessionId: String,\n        recaptchaToken: String? = null,\n        geeChallenge: String? = null,\n        geeValidate: String? = null,\n        geeSeccode: String? = null,\n        channel: String,\n        buvid: String,\n        statistics: String,\n        ts: Long\n    ): BiliResponse<SendSmsResponse> = client.post(\"/x/passport-login/sms/send\") {\n        setBody(FormDataContent(\n            Parameters.build {\n                append(\"cid\", \"$cid\")\n                append(\"tel\", \"$tel\")\n                append(\"login_session_id\", loginSessionId)\n                recaptchaToken?.let { append(\"recaptcha_token\", it) }\n                geeChallenge?.let { append(\"gee_challenge\", it) }\n                geeValidate?.let { append(\"gee_validate\", it) }\n                geeSeccode?.let { append(\"gee_seccode\", it) }\n                append(\"channel\", channel)\n                append(\"buvid\", buvid)\n                append(\"statistics\", statistics)\n                append(\"ts\", \"$ts\")\n            }\n        ))\n    }.body()\n\n    suspend fun loginWithSms(\n        cid: Long,\n        tel: Long,\n        loginSessionId: String,\n        code: Int,\n        captchaKey: String\n    ): BiliResponse<SmsLoginResponse> = client.post(\"/x/passport-login/login/sms\") {\n        setBody(FormDataContent(\n            Parameters.build {\n                append(\"cid\", \"$cid\")\n                append(\"tel\", \"$tel\")\n                append(\"login_session_id\", loginSessionId)\n                append(\"code\", \"$code\")\n                append(\"captcha_key\", captchaKey)\n                append(\"ts\", \"0\")\n            }\n        ))\n    }.body()\n\n    /**\n     * 获取buvid3\n     *\n     * @param source 获取来源 已知：main_web\n     */\n    suspend fun getbuvid3(): String {\n        val response = client.get(\"/x/web-frontend/getbuvid\") {\n            url{\n                host = \"api.bilibili.com\"\n                protocol = URLProtocol.HTTPS\n            }\n        }\n        return runCatching {\n            json.decodeFromString<BiliResponse<String>>(response.bodyAsText()).getResponseData()\n        }.getOrDefault(\"\")\n    }\n\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/BiliPlusHttpApi.kt",
    "content": "package dev.aaa1115910.biliapi.http\n\nimport dev.aaa1115910.biliapi.BiliApiConstants\nimport dev.aaa1115910.biliapi.http.entity.BiliResponse\nimport dev.aaa1115910.biliapi.http.entity.biliplus.View\nimport dev.aaa1115910.biliapi.http.plugins.BiliUserAgent\nimport dev.aaa1115910.biliapi.http.util.BiliDns\nimport io.ktor.client.HttpClient\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.HttpRequestRetry\nimport io.ktor.client.plugins.compression.ContentEncoding\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.request.get\nimport io.ktor.client.request.parameter\nimport io.ktor.client.statement.bodyAsText\nimport io.ktor.http.URLProtocol\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.decodeFromJsonElement\nimport kotlinx.serialization.json.int\nimport kotlinx.serialization.json.jsonObject\nimport kotlinx.serialization.json.jsonPrimitive\n\nobject BiliPlusHttpApi {\n    private var endPoint: String = \"www.biliplus.com\"\n    private lateinit var client: HttpClient\n\n    private val json = Json {\n        coerceInputValues = true\n        ignoreUnknownKeys = true\n        prettyPrint = true\n    }\n\n    init {\n        createClient()\n    }\n\n    private fun createClient() {\n        client = HttpClient(OkHttp) {\n            engine {\n                config {\n                    dns(BiliDns)\n                }\n            }\n            BiliUserAgent()\n            install(ContentNegotiation) {\n                json(json)\n            }\n            install(ContentEncoding) {\n                deflate(1.0F)\n                gzip(0.9F)\n            }\n            install(HttpRequestRetry) {\n                retryOnException(maxRetries = 2)\n            }\n            defaultRequest {\n                url {\n                    host = endPoint\n                    protocol = URLProtocol.HTTPS\n                }\n            }\n        }\n    }\n\n    suspend fun view(\n        aid: Long,\n        update: Boolean = true,\n        accessKey: String? = null\n    ): BiliResponse<View> {\n        val result = client.get(\"/api/view\") {\n            parameter(\"id\", aid)\n            parameter(\"update\", update)\n            accessKey?.let { parameter(\"access_key\", it) }\n        }.bodyAsText()\n        val resultJsonObject = json.parseToJsonElement(result).jsonObject\n        return if (resultJsonObject.size == 3) {\n            BiliResponse(\n                code = resultJsonObject[\"code\"]!!.jsonPrimitive.int,\n                message = resultJsonObject[\"message\"]!!.jsonPrimitive.content,\n                ttl = resultJsonObject[\"ttl\"]!!.jsonPrimitive.int,\n                data = null\n            )\n        } else {\n            BiliResponse(\n                code = 0,\n                message = \"success\",\n                ttl = 0,\n                result = json.decodeFromJsonElement<View>(resultJsonObject)\n            )\n        }\n    }\n\n    suspend fun getSeasonIdByAvid(\n        aid: Long\n    ): Int? {\n        return runCatching {\n            view(aid).getResponseData().bangumi?.seasonId?.toInt()\n        }.onFailure {\n            println(\"get season id by avid through biliplus failed: ${it.stackTraceToString()}\")\n        }.getOrDefault(null)\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/BiliResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity\n\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.serialization.Serializable\n\n/**\n * @param code 0：成功 -101：账号未登录 -400：参数错误 -401：非法访问 -403：访问权限不足\n */\n@Serializable\ndata class BiliResponse<T>(\n    val code: Int,\n    val message: String,\n    val ttl: Int? = null,\n    val data: T? = null,\n    val result: T? = null\n) {\n    companion object {\n        private val logger = KotlinLogging.logger {}\n    }\n    \n    init {\n        when (code) {\n            0 -> {}\n            -101 -> logger.error { \"请求失败，账号未登录: $message (code: $code)\" }\n            -352 -> logger.error { \"请求失败，风控异常: $message (code: $code)\" }\n            else -> logger.error { \"请求失败: $message (code: $code)\" }\n        }\n    }\n\n    @Throws()\n    fun getResponseData(): T {\n        when (code) {\n            0 -> {}\n            -101 -> throw AuthFailureException(message)\n            -352 -> throw RiskControlException(message)\n            87008 -> throw IllegalStateException(\"该视频为专属视频，需要充电才能观看 (code: $code)\")\n            else -> throw IllegalStateException(message)\n        }\n        check(data != null || result != null) { \"response data and result are both null\" }\n        data?.let { return it }\n        result?.let { return it }\n        error(\"response data and result are both null, and code should not run here\")\n    }\n}\n\n@Serializable\ndata class BiliResponseWithoutData(\n    val code: Int,\n    val message: String,\n    val ttl: Int\n) {\n    companion object {\n        private val logger = KotlinLogging.logger {}\n    }\n    \n    init {\n        when (code) {\n            0 -> {}\n            -101 -> logger.error { \"请求失败，账号未登录: $message (code: $code)\" }\n            -352 -> logger.error { \"请求失败，风控异常: $message (code: $code)\" }\n            else -> logger.error { \"请求失败: $message (code: $code)\" }\n        }\n    }\n}\n\n@Suppress(\"unused\")\nclass AuthFailureException : RuntimeException {\n    constructor() : super()\n    constructor(message: String?) : super(message)\n    constructor(message: String?, cause: Throwable?) : super(message, cause)\n    constructor(cause: Throwable?) : super(cause)\n}\n\n@Suppress(\"unused\")\nclass RiskControlException : RuntimeException {\n    constructor() : super()\n    constructor(message: String?) : super(message)\n    constructor(message: String?, cause: Throwable?) : super(message, cause)\n    constructor(cause: Throwable?) : super(cause)\n}\n\n/**\n * 风控 v_voucher 异常\n *\n * 当 API 返回 code=0 但 data 中仅包含 v_voucher 时抛出，\n * 需要通过 Geetest 验证后使用返回的 grisk_id 作为 gaia_vtoken 重试请求。\n */\nclass VVoucherException(val vVoucher: String) : RuntimeException(\"risk control v_voucher: $vVoucher\")"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/biliplus/View.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.biliplus\n\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\n\n@Serializable\ndata class View(\n    val aid: Long,\n    val author: String,\n    val bangumi: Bangumi? = null,\n    val coins: Int,\n    val created: Int,\n    @SerialName(\"created_at\")\n    val createdAt: String,\n    val description: String,\n    val favorites: Int,\n    val id: Int,\n    @SerialName(\"lastupdate\")\n    val lastUpdate: String,\n    @SerialName(\"lastupdatets\")\n    val lastUpdatets: Int,\n    val list: List<ListItem>,\n    val mid: Long,\n    val pic: String,\n    val play: Long,\n    val review: Int,\n    val tag: String,\n    val tid: Int,\n    val title: String,\n    val typename: String,\n    @SerialName(\"v2_app_api\")\n    val v2AppApi: V2AppApi,\n    val ver: Int,\n    @SerialName(\"video_review\")\n    val videoReview: Int\n) {\n    @Serializable\n    data class Bangumi(\n        val cover: String,\n        @SerialName(\"is_finish\")\n        val isFinish: String,\n        @SerialName(\"is_jump\")\n        val isJump: Int,\n        @SerialName(\"newest_ep_id\")\n        val newestEpId: String,\n        @SerialName(\"newest_ep_index\")\n        val newestEpIndex: String,\n        @SerialName(\"ogv_play_url\")\n        val ogvPlayUrl: String,\n        @SerialName(\"season_id\")\n        val seasonId: String,\n        val title: String,\n        @SerialName(\"total_count\")\n        val totalCount: String,\n        val weekday: String\n    )\n\n    @Serializable\n    data class ListItem(\n        val cid: Long,\n        val page: Int,\n        val part: String,\n        val type: String,\n        val vid: String\n    )\n\n    @Serializable\n    data class V2AppApi(\n        val aid: Long,\n        val bvid: String,\n        val cid: Long,\n        @SerialName(\"cm_config\")\n        val cmConfig: CmConfig,\n        val config: Config,\n        val copyright: Int,\n        val ctime: Int,\n        @SerialName(\"DagwUser\")\n        val dagwUser: List<JsonElement>,\n        @SerialName(\"DataCenterInfo\")\n        val dataCenterInfo: String,\n        val desc: String,\n        val dimension: Dimension,\n        @SerialName(\"dm_seg\")\n        val dmSeg: Int,\n        val duration: Int,\n        val `dynamic`: String,\n        @SerialName(\"InteractLabel\")\n        val interactLabel: String,\n        @SerialName(\"like_custom\")\n        val likeCustom: LikeCustom,\n        @SerialName(\"LiveOrderText\")\n        val liveOrderText: String,\n        val owner: Owner,\n        @SerialName(\"owner_ext\")\n        val ownerExt: OwnerExt,\n        val pages: List<Page>,\n        val paster: Paster? = null,\n        val pic: String,\n        @SerialName(\"play_param\")\n        val playParam: Int,\n        @SerialName(\"PlayToast\")\n        val playToast: JsonElement? = null,\n        @SerialName(\"premiere_resource\")\n        val premiereResource: JsonElement? = null,\n        @SerialName(\"pub_location\")\n        val pubLocation: String? = null,\n        val pubdate: Int,\n        @SerialName(\"redirect_url\")\n        val redirectUrl: String? = null,\n        @SerialName(\"RejectPage\")\n        val rejectPage: JsonElement? = null,\n        val rights: Rights,\n        val season: Season? = null,\n        @SerialName(\"share_subtitle\")\n        val shareSubtitle: String? = null,\n        @SerialName(\"short_link\")\n        val shortLink: String,\n        @SerialName(\"short_link_v2\")\n        val shortLinkV2: String,\n        val stat: Stat,\n        val state: Int,\n        @SerialName(\"t_icon\")\n        val tIcon: TIcon,\n        @SerialName(\"TabModule\")\n        val tabModule: JsonElement? = null,\n        val tag: List<Tag>,\n        val tid: Int,\n        val title: String,\n        val tname: String,\n        @SerialName(\"up_from_v2\")\n        val upFromV2: Int? = null,\n        val videos: Int,\n        @SerialName(\"vt_display\")\n        val vtDisplay: String\n    ) {\n        @Serializable\n        data class CmConfig(\n            @SerialName(\"ads_control\")\n            val adsControl: AdsControl\n        ) {\n            @Serializable\n            data class AdsControl(\n                @SerialName(\"has_danmu\")\n                val hasDanmu: Int,\n                @SerialName(\"under_player_scroller_seconds\")\n                val underPlayerScrollerSeconds: Int\n            )\n        }\n\n        @Serializable\n        data class Config(\n            @SerialName(\"abtest_small_window\")\n            val abtestSmallWindow: String,\n            @SerialName(\"feed_has_next\")\n            val feedHasNext: Boolean,\n            @SerialName(\"feed_style\")\n            val feedStyle: String,\n            @SerialName(\"has_guide\")\n            val hasGuide: Boolean,\n            @SerialName(\"is_absolute_time\")\n            val isAbsoluteTime: Boolean,\n            @SerialName(\"local_play\")\n            val localPlay: Int,\n            @SerialName(\"rec_three_point_style\")\n            val recThreePointStyle: Int,\n            @SerialName(\"relates_title\")\n            val relatesTitle: String,\n            @SerialName(\"share_style\")\n            val shareStyle: Int,\n            @SerialName(\"valid_show_m\")\n            val validShowM: Int,\n            @SerialName(\"valid_show_n\")\n            val validShowN: Int\n        )\n\n        @Serializable\n        data class Dimension(\n            val height: Int,\n            val rotate: Int,\n            val width: Int\n        )\n\n        @Serializable\n        data class LikeCustom(\n            @SerialName(\"full_to_half_progress\")\n            val fullToHalfProgress: Int,\n            @SerialName(\"like_switch\")\n            val likeSwitch: Boolean,\n            @SerialName(\"non_full_progress\")\n            val nonFullProgress: Int,\n            @SerialName(\"update_count\")\n            val updateCount: Int\n        )\n\n        @Serializable\n        data class Owner(\n            val face: String,\n            val mid: Int,\n            val name: String\n        )\n\n        @Serializable\n        data class OwnerExt(\n            @SerialName(\"arc_count\")\n            val arcCount: String,\n            val assists: JsonElement? = null,\n            val fans: Int,\n            @SerialName(\"nft_face_icon\")\n            val nftFaceIcon: JsonElement? = null,\n            @SerialName(\"official_verify\")\n            val officialVerify: OfficialVerify,\n            val vip: Vip\n        ) {\n            @Serializable\n            data class OfficialVerify(\n                val desc: String,\n                val type: Int\n            )\n\n            @Serializable\n            data class Vip(\n                val accessStatus: Int,\n                val dueRemark: String,\n                val label: Label,\n                val themeType: Int,\n                val vipDueDate: Long,\n                val vipStatus: Int,\n                val vipStatusWarn: String,\n                val vipType: Int\n            ) {\n                @Serializable\n                data class Label(\n                    @SerialName(\"bg_color\")\n                    val bgColor: String,\n                    @SerialName(\"bg_style\")\n                    val bgStyle: Int,\n                    @SerialName(\"border_color\")\n                    val borderColor: String,\n                    @SerialName(\"img_label_uri_hans\")\n                    val imgLabelUriHans: String,\n                    @SerialName(\"img_label_uri_hans_static\")\n                    val imgLabelUriHansStatic: String,\n                    @SerialName(\"img_label_uri_hant\")\n                    val imgLabelUriHant: String,\n                    @SerialName(\"img_label_uri_hant_static\")\n                    val imgLabelUriHantStatic: String,\n                    @SerialName(\"label_theme\")\n                    val labelTheme: String,\n                    val path: String,\n                    val text: String,\n                    @SerialName(\"text_color\")\n                    val textColor: String,\n                    @SerialName(\"use_img_label\")\n                    val useImgLabel: Boolean\n                )\n            }\n        }\n\n        @Serializable\n        data class Page(\n            val cid: Long,\n            val dimension: Dimension? = null,\n            val dmlink: String? = null,\n            @SerialName(\"download_subtitle\")\n            val downloadSubtitle: String? = null,\n            @SerialName(\"download_title\")\n            val downloadTitle: String? = null,\n            val duration: Int? = null,\n            val from: String,\n            val page: Int,\n            val part: String,\n            val vid: String,\n            val weblink: String? = null\n        ) {\n            @Serializable\n            data class Dimension(\n                val height: Int,\n                val rotate: Int,\n                val width: Int\n            )\n        }\n\n        @Serializable\n        data class Paster(\n            val aid: Long,\n            @SerialName(\"allow_jump\")\n            val allowJump: Int,\n            val cid: Long,\n            val duration: Int,\n            val type: Int,\n            val url: String\n        )\n\n        @Serializable\n        data class Rights(\n            @SerialName(\"arc_pay\")\n            val arcPay: Int,\n            val autoplay: Int,\n            val bp: Int,\n            val download: Int,\n            val elec: Int,\n            val hd5: Int,\n            @SerialName(\"is_cooperation\")\n            val isCooperation: Int,\n            val movie: Int,\n            @SerialName(\"no_background\")\n            val noBackground: Int,\n            @SerialName(\"no_reprint\")\n            val noReprint: Int,\n            val pay: Int,\n            @SerialName(\"pay_free_watch\")\n            val payFreeWatch: Int,\n            @SerialName(\"ugc_pay\")\n            val ugcPay: Int,\n            @SerialName(\"ugc_pay_preview\")\n            val ugcPayPreview: Int\n        )\n\n        @Serializable\n        data class Season(\n            val cover: String,\n            @SerialName(\"is_finish\")\n            val isFinish: String,\n            @SerialName(\"is_jump\")\n            val isJump: Int,\n            @SerialName(\"newest_ep_id\")\n            val newestEpId: String,\n            @SerialName(\"newest_ep_index\")\n            val newestEpIndex: String,\n            @SerialName(\"ogv_play_url\")\n            val ogvPlayUrl: String,\n            @SerialName(\"season_id\")\n            val seasonId: String,\n            val title: String,\n            @SerialName(\"total_count\")\n            val totalCount: String,\n            val weekday: String\n        )\n\n        @Serializable\n        data class Stat(\n            val aid: Long,\n            val coin: Int,\n            val danmaku: Int,\n            val dislike: Int,\n            val favorite: Int,\n            @SerialName(\"his_rank\")\n            val hisRank: Int,\n            val like: Int,\n            @SerialName(\"now_rank\")\n            val nowRank: Int,\n            val reply: Int,\n            val share: Int,\n            val view: Long,\n            val vt: Int,\n            val vv: Int\n        )\n\n        @Serializable\n        data class TIcon(\n            val act: Act,\n            val new: New\n        ) {\n            @Serializable\n            data class Act(\n                val icon: String\n            )\n\n            @Serializable\n            data class New(\n                val icon: String\n            )\n        }\n\n        @Serializable\n        data class Tag(\n            val attribute: Int,\n            val cover: String,\n            val hated: Int,\n            val hates: Int,\n            @SerialName(\"is_activity\")\n            val isActivity: Int,\n            val liked: Int,\n            val likes: Int,\n            @SerialName(\"tag_id\")\n            val tagId: Int,\n            @SerialName(\"tag_name\")\n            val tagName: String,\n            @SerialName(\"tag_type\")\n            val tagType: String,\n            val uri: String\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/danmaku/DanmakuResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.danmaku\n\n\ndata class DanmakuResponse(\n    val chatserver: String,\n    val chatId: Long,\n    val maxLimit: Int,\n    val state: Int,\n    val realName: Int,\n    val source: String,\n    val data: List<DanmakuData> = emptyList()\n)\n\ndata class DanmakuData(\n    val time: Float,\n    val type: Int,\n    val size: Int,\n    val color: Int,\n    val timestamp: Int,\n    val pool: Int,\n    val midHash: String,\n    val dmid: Long,\n    val level: Int = 0,\n    val text: String\n) {\n    companion object {\n        fun fromString(p: String, text: String): DanmakuData {\n            val data = p.split(\",\")\n            if (data.size < 9) {\n                throw IllegalArgumentException(\"Invalid danmaku data format: insufficient parameters\")\n            }\n            return DanmakuData(\n                time = data[0].toFloat(),\n                type = data[1].toInt(),\n                size = data[2].toInt(),\n                color = data[3].toInt(),\n                timestamp = data[4].toInt(),\n                pool = data[5].toInt(),\n                midHash = data[6],\n                dmid = data[7].toLong(),\n                level = data[8].toInt(),\n                text = text\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/dynamic/DynamicDetailResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.dynamic\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class DynamicDetailData(\n    val item: DynamicItem\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/dynamic/DynamicResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.dynamic\n\nimport dev.aaa1115910.biliapi.http.entity.user.Pendant\nimport dev.aaa1115910.biliapi.http.entity.user.Vip\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class DynamicData(\n    @SerialName(\"has_more\")\n    val hasMore: Boolean,\n    val offset: String,\n    @SerialName(\"update_baseline\")\n    val updateBaseline: String,\n    @SerialName(\"update_num\")\n    val updateNum: Int,\n    val items: List<DynamicItem> = emptyList()\n)\n\n/**\n * @param basic 基础信息\n * @param idStr 动态 id\n * @param modules 动态内容\n * @param orig 转发内容\n * @param type 动态类型\n * @param visible 是否可见\n */\n@Serializable\ndata class DynamicItem(\n    val basic: Basic,\n    @SerialName(\"id_str\")\n    val idStr: String? = null,\n    val modules: Modules,\n    val orig: DynamicItem? = null,\n    val type: String,\n    val visible: Boolean,\n    @SerialName(\"jump_url\")\n    val jumpUrl: String? = null\n) {\n    @Serializable\n    data class Basic(\n        @SerialName(\"comment_id_str\")\n        val commentIdStr: String,\n        @SerialName(\"comment_type\")\n        val commentType: Long,\n        @SerialName(\"jump_url\")\n        val jumpUrl: String? = null,\n        @SerialName(\"like_icon\")\n        val likeIcon: LikeIcon,\n        @SerialName(\"rid_str\")\n        val ridStr: String\n    ) {\n        @Serializable\n        data class LikeIcon(\n            @SerialName(\"action_url\")\n            val actionUrl: String,\n            @SerialName(\"end_url\")\n            val endUrl: String,\n            val id: Long,\n            @SerialName(\"start_url\")\n            val startUrl: String\n        )\n    }\n\n    /**\n     * @param moduleAuthor 作者信息\n     * @param moduleDynamic 动态内容\n     * @param moduleMore 更多菜单按钮信息 当位于转发内容 [DynamicItem.orig] 时，该项为 null\n     * @param moduleStat 动态底部按钮信息 当位于转发内容 [DynamicItem.orig] 时，该项为 null\n     */\n    @Serializable\n    data class Modules(\n        @SerialName(\"module_author\")\n        val moduleAuthor: Author,\n        @SerialName(\"module_dynamic\")\n        val moduleDynamic: Dynamic,\n        @SerialName(\"module_more\")\n        val moduleMore: More? = null,\n        @SerialName(\"module_stat\")\n        val moduleStat: Stat? = null\n    ) {\n        @Serializable\n        data class Author(\n            val face: String,\n            @SerialName(\"face_nft\")\n            val faceNft: Boolean,\n            val following: Boolean = false,\n            @SerialName(\"jump_url\")\n            val jumpUrl: String,\n            val label: String,\n            val mid: Long,\n            val name: String,\n            @SerialName(\"official_verify\")\n            val officialVerify: OfficialVerify? = null,\n            val pendant: Pendant? = null,\n            @SerialName(\"pub_action\")\n            val pubAction: String,\n            @SerialName(\"pub_location_text\")\n            val pubLocationText: String? = null,\n            @SerialName(\"pub_time\")\n            val pubTime: String,\n            @SerialName(\"pub_ts\")\n            val pubTs: Int,\n            val type: String,\n            val vip: Vip? = null\n        ) {\n            @Serializable\n            data class OfficialVerify(\n                val desc: String,\n                val type: Int\n            )\n        }\n\n        @Serializable\n        data class Dynamic(\n            val additional: Additional? = null,\n            val desc: Desc? = null,\n            val major: Major? = null,\n            val topic: Topic? = null\n        ) {\n            @Serializable\n            data class Additional(\n                val common: Common? = null,\n                val reserve: Reserve? = null,\n                val type: String\n            ) {\n                @Serializable\n                data class Common(\n                    val button: Button,\n                    val cover: String,\n                    val desc1: String,\n                    val desc2: String,\n                    @SerialName(\"head_text\")\n                    val headText: String,\n                    @SerialName(\"id_str\")\n                    val idStr: String,\n                    @SerialName(\"jump_url\")\n                    val jumpUrl: String,\n                    val style: Int,\n                    @SerialName(\"sub_type\")\n                    val subType: String,\n                    val title: String\n                )\n            }\n\n            @Serializable\n            data class Button(\n                val check: ButtonItem? = null,\n                val status: Int? = null,\n                val type: Int,\n                val uncheck: ButtonItem? = null,\n                @SerialName(\"jump_style\")\n                val jumpStyle: ButtonItem? = null,\n                @SerialName(\"jump_url\")\n                val jumpUrl: String? = null\n            ) {\n                @Serializable\n                data class ButtonItem(\n                    @SerialName(\"icon_url\")\n                    val iconUrl: String? = null,\n                    val text: String\n                )\n            }\n\n            @Serializable\n            data class Reserve(\n                val button: Button,\n                val desc1: Desc,\n                val desc2: Desc,\n                @SerialName(\"jump_url\")\n                val jumpUrl: String,\n                @SerialName(\"reserve_total\")\n                val reserveTotal: Int,\n                val rid: Long,\n                val state: Int,\n                val stypc: Int? = null,\n                val title: String,\n                @SerialName(\"up_mid\")\n                val upMid: Long\n            ) {\n                @Serializable\n                data class Desc(\n                    val style: Int,\n                    val text: String,\n                    val visible: Boolean? = null\n                )\n            }\n\n            @Serializable\n            data class Desc(\n                @SerialName(\"rich_text_nodes\")\n                val richTextNodes: List<RichTextNodeItem>,\n                val text: String\n            ) {\n                @Serializable\n                data class RichTextNodeItem(\n                    val emoji: Emoji? = null,\n                    @SerialName(\"orig_text\")\n                    val origText: String,\n                    val text: String,\n                    val type: String\n                ) {\n                    @Serializable\n                    data class Emoji(\n                        @SerialName(\"icon_url\")\n                        val iconUrl: String,\n                        val size: Int,\n                        val text: String,\n                        val type: Int\n                    )\n                }\n            }\n\n            /**\n             * 在一些情况下会出现数据存放的位置不一样的情况\n             * 例如默认情况下 draw 的文字会放在上一次级类的 desc 中，而浏览器里默认在请求时会带上一些 features，使内容放在 opus 内\n             */\n            @Serializable\n            data class Major(\n                val archive: Archive? = null,\n                @SerialName(\"live_rcmd\")\n                val liveRcmd: LiveRcmd? = null,\n                val opus: Opus? = null,\n                val draw: Draw? = null,\n                val pgc: Pgc? = null,\n                val article: Article? = null,\n                val none: None? = null,\n                @SerialName(\"ugc_season\")\n                val ugcSeason: UgcSeason? = null,\n                val type: String\n            ) {\n                @Serializable\n                data class Archive(\n                    val aid: String,\n                    val badge: Badge,\n                    val bvid: String,\n                    val cover: String,\n                    val desc: String,\n                    @SerialName(\"disable_preview\")\n                    val disablePreview: Int,\n                    @SerialName(\"duration_text\")\n                    val durationText: String,\n                    @SerialName(\"jump_url\")\n                    val jumpUrl: String,\n                    val stat: Stat,\n                    val title: String,\n                    val type: Int\n                ) {\n                    @Serializable\n                    data class Badge(\n                        @SerialName(\"bg_color\")\n                        val bgColor: String,\n                        val color: String,\n                        val text: String\n                    )\n\n                    @Serializable\n                    data class Stat(\n                        val danmaku: String,\n                        val play: String\n                    )\n                }\n\n                @Serializable\n                data class LiveRcmd(\n                    val content: String,\n                    @SerialName(\"reserve_type\")\n                    val reserveType: Int\n                )\n\n                /**\n                 * @param foldAction [展开,收起]\n                 * @param jumpUrl 跳转地址\n                 * @param pics 动态内的图片\n                 * @param summary 动态内的文字\n                 */\n                @Serializable\n                data class Opus(\n                    @SerialName(\"fold_action\")\n                    val foldAction: List<String>,\n                    @SerialName(\"jump_url\")\n                    val jumpUrl: String,\n                    val pics: List<Pic>,\n                    val summary: Desc,\n                    val title: String? = null\n                ) {\n                    @Serializable\n                    data class Pic(\n                        val height: Int,\n                        val width: Int,\n                        val size: Float? = null,\n                        val url: String\n                    )\n                }\n\n                @Serializable\n                data class Draw(\n                    val id: Int,\n                    val items: List<Pic>,\n                ) {\n                    @Serializable\n                    data class Pic(\n                        val height: Int,\n                        val width: Int,\n                        val size: Float? = null,\n                        val src: String,\n                        val tags: List<String>\n                    )\n                }\n\n                @Serializable\n                data class Pgc(\n                    val badge: Archive.Badge,\n                    val cover: String,\n                    val epid: Int,\n                    @SerialName(\"jump_url\")\n                    val jumpUrl: String,\n                    @SerialName(\"season_id\")\n                    val seasonId: Int,\n                    val stat: Archive.Stat,\n                    @SerialName(\"sub_type\")\n                    val subType: Int,\n                    val title: String,\n                    val type: Int\n                )\n\n                @Serializable\n                data class Article(\n                    val covers: List<String>,\n                    val desc: String,\n                    val id: Int,\n                    @SerialName(\"jump_url\")\n                    val jumpUrl: String,\n                    val label: String,\n                    val title: String\n                )\n\n                @Serializable\n                data class None(\n                    val tips: String\n                )\n\n                @Serializable\n                data class UgcSeason(\n                    val aid: Long,\n                    val badge: Archive.Badge,\n                    val bvid: String,\n                    val cover: String,\n                    val desc: String? = null,\n                    @SerialName(\"disable_preview\")\n                    val disablePreview: Int,\n                    @SerialName(\"duration_text\")\n                    val durationText: String,\n                    @SerialName(\"jump_url\")\n                    val jumpUrl: String,\n                    val stat: Archive.Stat,\n                    val title: String,\n                    val type: Int\n                )\n            }\n\n            @Serializable\n            data class Topic(\n                val id: Int,\n                @SerialName(\"jump_url\")\n                val jumpUrl: String,\n                val name: String\n            )\n        }\n\n        @Serializable\n        data class More(\n            @SerialName(\"three_point_items\")\n            val threePointItems: List<MoreItem> = emptyList()\n        ) {\n            @Serializable\n            data class MoreItem(\n                val label: String,\n                val type: String\n            )\n        }\n\n        @Serializable\n        data class Stat(\n            val comment: StatItem,\n            val forward: StatItem,\n            val like: StatItem\n        ) {\n            @Serializable\n            data class StatItem(\n                val count: Int,\n                val forbidden: Boolean,\n                val statue: Boolean = false\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/history/HistoryData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.history\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 历史记录信息\n *\n * @param cursor 历史记录页面信息\n * @param tab 历史记录筛选类型\n * @param list 分段历史记录列表\n */\n@Serializable\ndata class HistoryData(\n    val cursor: Cursor,\n    val tab: List<TabItem>,\n    val list: List<HistoryItem>\n) {\n    /**\n     * 历史记录页面信息\n     *\n     * @param max 最后一项目标 id 见请求参数\n     * @param viewAt 最后一项时间节点 时间戳\n     * @param business 最后一项业务类型 见请求参数\n     * @param ps 每页项数\n     */\n    @Serializable\n    data class Cursor(\n        val max: Long,\n        @SerialName(\"view_at\")\n        val viewAt: Long,\n        val business: String,\n        val ps: Int\n    )\n\n    /**\n     * 历史记录筛选类型\n     *\n     * @param type 类型\n     * @param name 类型名\n     */\n    @Serializable\n    data class TabItem(\n        val type: String,\n        val name: String\n    )\n}\n\n/**\n * 历史记录列表项\n *\n * @param title 条目标题\n * @param longTitle 条目副标题\n * @param cover 条目封面图 url 用于专栏以外的条目\n * @param covers 条目封面图组 仅用于专栏 有效时：array 无效时：null\n * @param uri 重定向 url 仅用于剧集和直播\n * @param history  条目详细信息\n * @param videos 视频分 P 数目 仅用于稿件视频\n * @param authorName UP 主昵称\n * @param authorFace UP 主头像 url\n * @param authorMid UP 主 mid\n * @param viewAt 查看时间 时间戳\n * @param progress 视频观看进度 单位为秒 用于稿件视频或剧集\n * @param badge 角标文案 稿件视频 / 剧集 / 笔记\n * @param showTitle 分 P 标题 用于稿件视频或剧集\n * @param duration 视频总时长 用于稿件视频或剧集\n * @param current (?)\n * @param total 总计分集数 仅用于剧集\n * @param newDesc 最新一话 / 最新一 P 标识 用于稿件视频或剧集\n * @param isFinish 是否已完结\t仅用于剧集 0：未完结 1：已完结\n * @param isFav 是否收藏 0：未收藏 1：已收藏\n * @param kid 条目目标 id 详细内容见参数\n * @param tagName 子分区名 用于稿件视频和直播\n * @param liveStatus 直播状态 仅用于直播 0：未开播 1：已开播\n */\n@Serializable\ndata class HistoryItem(\n    val title: String,\n    @SerialName(\"long_title\")\n    val longTitle: String,\n    val cover: String,\n    val covers: List<String>? = null,\n    val uri: String,\n    val history: HistoryInfo,\n    val videos: Int,\n    @SerialName(\"author_name\")\n    val authorName: String,\n    @SerialName(\"author_face\")\n    val authorFace: String,\n    @SerialName(\"author_mid\")\n    val authorMid: Long,\n    @SerialName(\"view_at\")\n    val viewAt: Long,\n    val progress: Int,\n    val badge: String,\n    @SerialName(\"show_title\")\n    val showTitle: String,\n    val duration: Int,\n    val current: String,\n    val total: Int,\n    @SerialName(\"new_desc\")\n    val newDesc: String,\n    @SerialName(\"is_finish\")\n    val isFinish: Int,\n    @SerialName(\"is_fav\")\n    val isFav: Int,\n    val kid: Long,\n    @SerialName(\"tag_name\")\n    val tagName: String,\n    @SerialName(\"live_status\")\n    val liveStatus: Int\n) {\n    /**\n     * 历史记录详细信息\n     *\n     * @param oid 目标id 稿件视频&剧集（当business=archive或business=pgc时）：稿件avid 直播（当business=live时）：直播间id 文章（当business=article时）：文章cvid 文集（当business=article-list时）：文集rlid\n     * @param epid 剧集epid 仅用于剧集\n     * @param bvid 稿件bvid 仅用于稿件视频\n     * @param page 观看到的视频分P数 仅用于稿件视频\n     * @param cid 观看到的对象id 稿件视频&剧集（当business=archive或business=pgc时）：视频cid 文集（当business=article-list时）：文章cvid\n     * @param part 观看到的视频分 P 标题 仅用于稿件视频\n     * @param business 业务类型 见请求参数\n     * @param dt 记录查看的平台代码\t1 3 5 7：手机端 2：web端 4 6：pad端 33：TV端 0：其他\n     */\n    @Serializable\n    data class HistoryInfo(\n        val oid: Long,\n        val epid: Int,\n        val bvid: String,\n        val page: Int,\n        val cid: Long,\n        val part: String,\n        val business: String,\n        val dt: Int\n    )\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/home/RcmdIndexData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.home\n\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\n\n@Serializable\ndata class RcmdIndexData(\n    val config: Config,\n    @SerialName(\"interest_choose\")\n    val interestChoose: JsonElement? = null,\n    val items: List<RcmdItem>\n) {\n    @Serializable\n    data class Config(\n        @SerialName(\"auto_refresh_by_behavior\")\n        val autoRefreshByBehavior: Int? = null,\n        @SerialName(\"auto_refresh_time\")\n        val autoRefreshTime: Int,\n        @SerialName(\"auto_refresh_time_by_active\")\n        val autoRefreshTimeByActive: Int,\n        @SerialName(\"auto_refresh_time_by_appear\")\n        val autoRefreshTimeByAppear: Int,\n        @SerialName(\"auto_refresh_time_by_behavior\")\n        val autoRefreshTimeByBehavior: Int? = null,\n        @SerialName(\"autoplay_card\")\n        val autoplayCard: Int,\n        @SerialName(\"card_density_exp\")\n        val cardDensityExp: Int,\n        val column: Int,\n        @SerialName(\"enable_rcmd_guide\")\n        val enableRcmdGuide: Boolean,\n        @SerialName(\"feed_clean_abtest\")\n        val feedCleanAbtest: Int,\n        @SerialName(\"history_cache_size\")\n        val historyCacheSize: Int? = null,\n        @SerialName(\"home_transfer_test\")\n        val homeTransferTest: Int,\n        @SerialName(\"inline_sound\")\n        val inlineSound: Int,\n        @SerialName(\"is_back_to_homepage\")\n        val isBackToHomepage: Boolean,\n        @SerialName(\"show_inline_danmaku\")\n        val showInlineDanmaku: Int,\n        @SerialName(\"single_autoplay_flag\")\n        val singleAutoplayFlag: Int? = null,\n        @SerialName(\"small_cover_wh_ratio\")\n        val smallCoverWhRatio: Double? = null,\n        @SerialName(\"space_enlarge_exp\")\n        val spaceEnlargeExp: Int? = null,\n        @SerialName(\"story_mode_v2_guide_exp\")\n        val storyModeV2GuideExp: Int,\n        val toast: JsonElement,\n        @SerialName(\"trigger_loadmore_left_line_num\")\n        val triggerLoadmoreLeftLineNum: Int? = null,\n        @SerialName(\"video_mode\")\n        val videoMode: Int? = null,\n        @SerialName(\"visible_area\")\n        val visibleArea: Int\n    )\n\n    @Serializable\n    data class RcmdItem(\n        //@SerialName(\"ad_info\")\n        //val adInfo: AdInfo,\n        val args: Args,\n        @SerialName(\"can_play\")\n        val canPlay: Int? = null,\n        @SerialName(\"card_goto\")\n        val cardGoto: String,\n        @SerialName(\"card_type\")\n        val cardType: String,\n        val cover: String? = null,\n        @SerialName(\"cover_left_1_content_description\")\n        val coverLeft1ContentDescription: String? = null,\n        @SerialName(\"cover_left_2_content_description\")\n        val coverLeft2ContentDescription: String? = null,\n        @SerialName(\"cover_left_icon_1\")\n        val coverLeftIcon1: Int? = null,\n        @SerialName(\"cover_left_icon_2\")\n        val coverLeftIcon2: Int? = null,\n        @SerialName(\"cover_left_text_1\")\n        val coverLeftText1: String? = null,\n        @SerialName(\"cover_left_text_2\")\n        val coverLeftText2: String? = null,\n        @SerialName(\"cover_right_content_description\")\n        val coverRightContentDescription: String? = null,\n        @SerialName(\"cover_right_text\")\n        val coverRightText: String? = null,\n        val desc: String? = null,\n        @SerialName(\"desc_button\")\n        val descButton: DescButton? = null,\n        @SerialName(\"ff_cover\")\n        val ffCover: String? = null,\n        val goto: String? = null,\n        @SerialName(\"goto_icon\")\n        val gotoIcon: GotoIcon? = null,\n        val idx: Int,\n        @SerialName(\"official_icon\")\n        val officialIcon: Int? = null,\n        @SerialName(\"param\")\n        val `param`: String? = null,\n        @SerialName(\"player_args\")\n        val playerArgs: PlayerArgs? = null,\n        @SerialName(\"rcmd_reason\")\n        val rcmdReason: String? = null,\n        @SerialName(\"rcmd_reason_style\")\n        val rcmdReasonStyle: RcmdReasonStyle? = null,\n        @SerialName(\"report_flow_data\")\n        val reportFlowData: String? = null,\n        @SerialName(\"talk_back\")\n        val talkBack: String? = null,\n        @SerialName(\"three_point\")\n        val threePoint: ThreePoint? = null,\n        @SerialName(\"three_point_v2\")\n        val threePointV2: List<ThreePointV2>,\n        val title: String? = null,\n        @SerialName(\"track_id\")\n        val trackId: String? = null,\n        val uri: String? = null\n    ) {\n        @Serializable\n        data class Args(\n            val aid: Long? = null,\n            val rid: Int? = null,\n            val rname: String? = null,\n            val tid: Int? = null,\n            val tname: String? = null,\n            @SerialName(\"up_id\")\n            val upId: Long? = null,\n            @SerialName(\"up_name\")\n            val upName: String? = null\n        )\n\n        @Serializable\n        data class DescButton(\n            val event: String,\n            val text: String,\n            val type: Int,\n            val uri: String? = null\n        )\n\n        @Serializable\n        data class GotoIcon(\n            @SerialName(\"icon_height\")\n            val iconHeight: Int,\n            @SerialName(\"icon_night_url\")\n            val iconNightUrl: String,\n            @SerialName(\"icon_url\")\n            val iconUrl: String,\n            @SerialName(\"icon_width\")\n            val iconWidth: Int\n        )\n\n        @Serializable\n        data class PlayerArgs(\n            val aid: Long,\n            val cid: Long,\n            val duration: Int,\n            val type: String\n        )\n\n        @Serializable\n        data class RcmdReasonStyle(\n            @SerialName(\"bg_color\")\n            val bgColor: String,\n            @SerialName(\"bg_color_night\")\n            val bgColorNight: String,\n            @SerialName(\"bg_style\")\n            val bgStyle: Int,\n            @SerialName(\"border_color\")\n            val borderColor: String,\n            @SerialName(\"border_color_night\")\n            val borderColorNight: String,\n            val text: String,\n            @SerialName(\"text_color\")\n            val textColor: String,\n            @SerialName(\"text_color_night\")\n            val textColorNight: String\n        )\n\n        @Serializable\n        data class ThreePoint(\n            @SerialName(\"dislike_reasons\")\n            val dislikeReasons: List<DislikeReason>,\n            val feedbacks: List<Feedback>? = null,\n            @SerialName(\"watch_later\")\n            val watchLater: Int? = null\n        ) {\n            @Serializable\n            data class DislikeReason(\n                val id: Int,\n                val name: String,\n                val toast: String\n            )\n\n            @Serializable\n            data class Feedback(\n                val id: Int,\n                val name: String,\n                val toast: String\n            )\n        }\n\n        @Serializable\n        data class ThreePointV2(\n            val icon: String? = null,\n            val reasons: List<Reason> = emptyList(),\n            val subtitle: String? = null,\n            val title: String? = null,\n            val type: String\n        ) {\n            @Serializable\n            data class Reason(\n                val id: Int,\n                val name: String,\n                val toast: String\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/home/RcmdTopData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.home\n\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\n\n@Serializable\ndata class RcmdTopData(\n    @SerialName(\"business_card\")\n    val businessCard: JsonElement? = null,\n    @SerialName(\"floor_info\")\n    val floorInfo: JsonElement? = null,\n    val item: List<RcmdItem>,\n    val mid: Long,\n    @SerialName(\"preload_expose_pct\")\n    val preloadExposePct: Double,\n    @SerialName(\"preload_floor_expose_pct\")\n    val preloadFloorExposePct: Double,\n    @SerialName(\"side_bar_column\")\n    val sideBarColumn: List<SideBarColumn>? = emptyList(),\n    @SerialName(\"user_feature\")\n    val userFeature: JsonElement? = null\n) {\n    @Serializable\n    data class RcmdItem(\n        @SerialName(\"av_feature\")\n        val avFeature: JsonElement? = null,\n        @SerialName(\"business_info\")\n        val businessInfo: BusinessInfo?,\n        val bvid: String,\n        val cid: Long,\n        val duration: Int,\n        @SerialName(\"enable_vt\")\n        val enableVt: Int,\n        val goto: String,\n        val id: Long,\n        @SerialName(\"is_followed\")\n        val isFollowed: Int,\n        @SerialName(\"is_stock\")\n        val isStock: Int,\n        @SerialName(\"ogv_info\")\n        val ogvInfo: JsonElement? = null,\n        val owner: Owner? = null,\n        val pic: String,\n        val pos: Int,\n        val pubdate: Int,\n        @SerialName(\"rcmd_reason\")\n        val rcmdReason: RcmdReason? = null,\n        @SerialName(\"room_info\")\n        val roomInfo: JsonElement? = null,\n        @SerialName(\"show_info\")\n        val showInfo: Int,\n        val stat: Stat? = null,\n        val title: String,\n        @SerialName(\"track_id\")\n        val trackId: String,\n        val uri: String\n    ) {\n        @Serializable\n        data class BusinessInfo(\n            @SerialName(\"activity_type\")\n            val activityType: Int,\n            @SerialName(\"ad_cb\")\n            val adCb: String,\n            @SerialName(\"ad_desc\")\n            val adDesc: String,\n            @SerialName(\"adver_name\")\n            val adverName: String,\n            val agency: String,\n            val area: Int,\n            @SerialName(\"asg_id\")\n            val asgId: Int,\n            @SerialName(\"business_mark\")\n            val businessMark: BusinessMark,\n            @SerialName(\"card_type\")\n            val cardType: Int,\n            @SerialName(\"cm_mark\")\n            val cmMark: Int,\n            @SerialName(\"contract_id\")\n            val contractId: String,\n            @SerialName(\"creative_id\")\n            val creativeId: Int,\n            @SerialName(\"creative_type\")\n            val creativeType: Int,\n            val epid: Int,\n            val id: Int,\n            @SerialName(\"inline\")\n            val `inline`: Inline,\n            val intro: String,\n            @SerialName(\"is_ad\")\n            val isAd: Boolean,\n            @SerialName(\"is_ad_loc\")\n            val isAdLoc: Boolean,\n            val label: String,\n            val litpic: String,\n            val mid: String,\n            val name: String,\n            @SerialName(\"null_frame\")\n            val nullFrame: Boolean,\n            val operater: String,\n            val pic: String,\n            @SerialName(\"pic_main_color\")\n            val picMainColor: String,\n            @SerialName(\"pos_num\")\n            val posNum: Int,\n            @SerialName(\"request_id\")\n            val requestId: String,\n            @SerialName(\"res_id\")\n            val resId: Int,\n            @SerialName(\"server_type\")\n            val serverType: Int,\n            @SerialName(\"src_id\")\n            val srcId: Int,\n            val stime: Int,\n            val style: Int,\n            @SerialName(\"sub_title\")\n            val subTitle: String,\n            val title: String,\n            val url: String\n        ) {\n            @Serializable\n            data class BusinessMark(\n                @SerialName(\"bg_border_color\")\n                val bgBorderColor: String,\n                @SerialName(\"bg_color\")\n                val bgColor: String,\n                @SerialName(\"bg_color_night\")\n                val bgColorNight: String,\n                @SerialName(\"border_color\")\n                val borderColor: String,\n                @SerialName(\"border_color_night\")\n                val borderColorNight: String,\n                @SerialName(\"img_height\")\n                val imgHeight: Int,\n                @SerialName(\"img_url\")\n                val imgUrl: String,\n                @SerialName(\"img_width\")\n                val imgWidth: Int,\n                val text: String,\n                @SerialName(\"text_color\")\n                val textColor: String,\n                @SerialName(\"text_color_night\")\n                val textColorNight: String,\n                val type: Int\n            )\n\n            @Serializable\n            data class Inline(\n                @SerialName(\"inline_barrage_switch\")\n                val inlineBarrageSwitch: Int,\n                @SerialName(\"inline_type\")\n                val inlineType: Int,\n                @SerialName(\"inline_url\")\n                val inlineUrl: String,\n                @SerialName(\"inline_use_same\")\n                val inlineUseSame: Int\n            )\n        }\n\n        @Serializable\n        data class Owner(\n            val face: String,\n            val mid: Long,\n            val name: String\n        )\n\n        @Serializable\n        data class RcmdReason(\n            val content: String? = null,\n            @SerialName(\"reason_type\")\n            val reasonType: Int\n        )\n\n        @Serializable\n        data class Stat(\n            val danmaku: Int,\n            val like: Int,\n            val view: Long,\n            val vt: Int\n        )\n    }\n\n    @Serializable\n    data class SideBarColumn(\n        @SerialName(\"av_feature\")\n        val avFeature: JsonElement? = null,\n        @SerialName(\"card_type\")\n        val cardType: String,\n        @SerialName(\"card_type_en\")\n        val cardTypeEn: String,\n        val comic: JsonElement? = null,\n        val cover: String,\n        val duration: Int,\n        @SerialName(\"enable_vt\")\n        val enableVt: Int,\n        val goto: String,\n        @SerialName(\"horizontal_cover_16_10\")\n        val horizontalCover1610: String? = null,\n        @SerialName(\"horizontal_cover_16_9\")\n        val horizontalCover169: String? = null,\n        val id: Int,\n        @SerialName(\"is_finish\")\n        val isFinish: Int,\n        @SerialName(\"is_play\")\n        val isPlay: Int,\n        @SerialName(\"is_rec\")\n        val isRec: Int,\n        @SerialName(\"is_started\")\n        val isStarted: Int,\n        @SerialName(\"new_ep\")\n        val newEp: NewEp,\n        val pos: Int,\n        val producer: List<Producer>,\n        @SerialName(\"room_info\")\n        val roomInfo: JsonElement? = null,\n        val source: String,\n        val stats: Stats,\n        val styles: List<String> = emptyList(),\n        @SerialName(\"sub_title\")\n        val subTitle: String,\n        val title: String,\n        @SerialName(\"track_id\")\n        val trackId: String,\n        val url: String\n    ) {\n        @Serializable\n        data class NewEp(\n            val cover: String,\n            @SerialName(\"day_of_week\")\n            val dayOfWeek: Int,\n            val duration: Int,\n            val id: Int,\n            @SerialName(\"index_show\")\n            val indexShow: String? = null,\n            @SerialName(\"long_title\")\n            val longTitle: String? = null,\n            @SerialName(\"pub_time\")\n            val pubTime: String?,\n            val title: String\n        )\n\n        @Serializable\n        data class Producer(\n            @SerialName(\"is_contribute\")\n            val isContribute: Int,\n            val mid: Long,\n            val name: String,\n            val type: Int\n        )\n\n        @Serializable\n        data class Stats(\n            val coin: Int,\n            val danmaku: Int,\n            val favorite: Int,\n            val follow: Int,\n            val likes: Int,\n            val reply: Int,\n            @SerialName(\"series_follow\")\n            val seriesFollow: Int? = null,\n            @SerialName(\"series_view\")\n            val seriesView: Int? = null,\n            val view: Long\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/index/IndexFilter.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.index\n\nval indexFilterSeasonVersion = mapOf(\n    -1 to \"全部\",\n    1 to \"正片\",\n    2 to \"电影\",\n    3 to \"其他\",\n)\n\nval indexFilterSpokenLanguageType = mapOf(\n    -1 to \"全部\",\n    1 to \"原声\",\n    2 to \"中文配音\",\n)\n\nval indexFilterArea = mapOf(\n    -1 to \"全部\",\n    2 to \"日本\",\n    3 to \"美国\",\n    4 to \"其他\",\n)\n\nval indexFilterIsFinish = mapOf(\n    -1 to \"全部\",\n    1 to \"完结\",\n    0 to \"连载\",\n)\n\nval indexFilterCopyright = mapOf(\n    -1 to \"全部\",\n    3 to \"独家\",\n    1 to \"其他\",\n)\n\nval indexFilterSeasonStatus = mapOf(\n    -1 to \"全部\",\n    1 to \"免费\",\n    2 to \"付费\",\n    4 to \"大会员\",\n)\n\nval indexFilterSeasonMonth = mapOf(\n    -1 to \"全部\",\n    1 to \"1月\",\n    4 to \"4月\",\n    7 to \"7月\",\n    10 to \"10月\",\n)\n\nval indexFilterYear = mapOf(\n    \"-1\" to \"全部\",\n    \"[2026,2027)\" to \"2026\",\n    \"[2025,2026)\" to \"2025\",\n    \"[2024,2025)\" to \"2024\",\n    \"[2023,2024)\" to \"2023\",\n    \"[2022,2023)\" to \"2022\",\n    \"[2021,2022)\" to \"2021\",\n    \"[2020,2021)\" to \"2020\",\n    \"[2019,2020)\" to \"2019\",\n    \"[2018,2019)\" to \"2018\",\n    \"[2017,2018)\" to \"2017\",\n    \"[2016,2017)\" to \"2016\",\n    \"[2015,2016)\" to \"2015\",\n    \"[2010,2015)\" to \"2015-2010\",\n    \"[2005,2010)\" to \"2009-2005\",\n    \"[2000,2005)\" to \"2004-2000\",\n    \"[1990,2000)\" to \"90年代\",\n    \"[1980,1990)\" to \"80年代\",\n    \"[,1980)\" to \"更早\",\n)\n\nval indexFilterStyleIdsAnime get() = IndexFilterStyle.animeStyles\nval indexFilterStyleIdsMovie get() = IndexFilterStyle.movieStyles\nval indexFilterStyleIdsDocumentary get() = IndexFilterStyle.documentaryStyles\nval indexFilterStyleIdsTV get() = IndexFilterStyle.tvStyles\nval indexFilterStyleIdsGuochuang get() = IndexFilterStyle.guochuangStyles\nval indexFilterStyleIdsVariety get() = IndexFilterStyle.varietyStyles\n\nval indexFilterAreaMovie get() = IndexFilterArea.movieAreas\nval indexFilterAreaTV get() = IndexFilterArea.tvAreas\n\nval indexFilterProducerId get() = IndexFilterProducerId.producerIds\n\nval indexFilterReleaseDate = mapOf(\n    \"-1\" to \"全部\",\n    \"[2026-01-01 00:00:00,2027-01-01 00:00:00)\" to \"2026\",\n    \"[2025-01-01 00:00:00,2026-01-01 00:00:00)\" to \"2025\",\n    \"[2024-01-01 00:00:00,2025-01-01 00:00:00)\" to \"2024\",\n    \"[2023-01-01 00:00:00,2024-01-01 00:00:00)\" to \"2023\",\n    \"[2022-01-01 00:00:00,2023-01-01 00:00:00)\" to \"2022\",\n    \"[2021-01-01 00:00:00,2022-01-01 00:00:00)\" to \"2021\",\n    \"[2020-01-01 00:00:00,2021-01-01 00:00:00)\" to \"2020\",\n    \"[2019-01-01 00:00:00,2020-01-01 00:00:00)\" to \"2019\",\n    \"[2018-01-01 00:00:00,2019-01-01 00:00:00)\" to \"2018\",\n    \"[2017-01-01 00:00:00,2018-01-01 00:00:00)\" to \"2017\",\n    \"[2016-01-01 00:00:00,2017-01-01 00:00:00)\" to \"2016\",\n    \"[2010-01-01 00:00:00,2016-01-01 00:00:00)\" to \"2015-2010\",\n    \"[2005-01-01 00:00:00,2010-01-01 00:00:00)\" to \"2009-2005\",\n    \"[2000-01-01 00:00:00,2005-01-01 00:00:00)\" to \"2004-2000\",\n    \"[1990-01-01 00:00:00,2000-01-01 00:00:00)\" to \"90年代\",\n    \"[1980-01-01 00:00:00,1990-01-01 00:00:00)\" to \"80年代\",\n    \"[,1980-01-01 00:00:00)\" to \"更早\",\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/index/IndexFilterArea.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.index\n\nobject IndexFilterArea {\n    private val areaFilter = mapOf(\n        -1 to \"全部\",\n        1 to \"中国大陆\",\n        2 to \"日本\",\n        3 to \"美国\",\n        4 to \"英国\",\n        5 to \"其他\",\n        6 to \"中国港台\",\n        8 to \"韩国\",\n        9 to \"法国\",\n        10 to \"泰国\",\n        13 to \"西班牙\",\n        15 to \"德国\",\n        35 to \"意大利\",\n    )\n\n    private val movieAreaIds = listOf(\n        -1, 1, 6, 3, 28, 9, 4, 15, 10, 35, 13, 5\n    )\n    private val tvAreaIds = listOf(\n        -1, 1, 2, 3, 4, 10, 5\n    )\n\n    val movieAreas by lazy { movieAreaIds.associateWith { areaFilter[it]!! } }\n    val tvAreas by lazy { tvAreaIds.associateWith { areaFilter[it]!! } }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/index/IndexFilterProducerId.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.index\n\nobject IndexFilterProducerId {\n    private val producerIdFilter = mapOf(\n        -1 to \"全部\",\n        1 to \"BBC\",\n        2 to \"NHK\",\n        3 to \"SKY\",\n        4 to \"央视\",\n        5 to \"ITV\",\n        6 to \"历史频道\",\n        7 to \"探索频道\",\n        8 to \"卫视\",\n        9 to \"自制\",\n        10 to \"ZDF\",\n        11 to \"合作机构\",\n        12 to \"国内其他\",\n        13 to \"国外其他\",\n        14 to \"国家地理\",\n        15 to \"索尼\",\n        16 to \"环球\",\n        17 to \"派拉蒙\",\n        18 to \"华纳\",\n        19 to \"迪士尼\",\n        20 to \"HBO\",\n\n        )\n\n    private val producerIdIds = listOf(\n        -1, 4, 1, 7, 14, 2, 6, 8, 9, 5, 3, 10, 11, 12, 13, 15, 16, 17, 18, 19, 20\n    )\n\n    val producerIds by lazy { producerIdIds.associateWith { producerIdFilter[it]!! } }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/index/IndexFilterStyle.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.index\n\nobject IndexFilterStyle {\n    private val styleFilter = mapOf(\n        -1 to \"全部\",\n        -10 to \"电影\",\n\n        10010 to \"原创\",\n        10011 to \"漫画改\",\n        10012 to \"小说改\",\n        10013 to \"游戏改\",\n        10014 to \"动态漫\",\n        10015 to \"布袋戏\",\n        10016 to \"热血\",\n        10017 to \"穿越\",\n        10018 to \"奇幻\",\n        10019 to \"玄幻\",\n\n        10020 to \"战斗\",\n        10021 to \"搞笑\",\n        10022 to \"日常\",\n        10023 to \"科幻\",\n        10024 to \"萌系\",\n        10025 to \"治愈\",\n        10026 to \"校园\",\n        10027 to \"少儿\",\n        10028 to \"泡面\",\n        10029 to \"恋爱\",\n\n        10030 to \"少女\",\n        10031 to \"魔法\",\n        10032 to \"冒险\",\n        10033 to \"历史\",\n        10034 to \"架空\",\n        10035 to \"机战\",\n        10036 to \"神魔\",\n        10037 to \"声控\",\n        10038 to \"运动\",\n        10039 to \"励志\",\n\n        10040 to \"音乐\",\n        10041 to \"推理\",\n        10042 to \"社团\",\n        10043 to \"智斗\",\n        10044 to \"催泪\",\n        10045 to \"美食\",\n        10046 to \"偶像\",\n        10047 to \"乙女\",\n        10048 to \"职场\",\n        10049 to \"古风\",\n\n        10050 to \"剧情\",\n        10051 to \"喜剧\",\n        10052 to \"爱情\",\n        10053 to \"动作\",\n        10054 to \"恐怖\",\n        10055 to \"犯罪\",\n        10056 to \"惊悚\",\n        10057 to \"悬疑\",\n        10058 to \"战争\",\n        //10059\n\n        10060 to \"传记\",\n        10061 to \"家庭\",\n        10062 to \"歌剧\",\n        10063 to \"纪实\",\n        10064 to \"灾难\",\n        10065 to \"人文\",\n        10066 to \"科技\",\n        10067 to \"探险\",\n        10068 to \"宇宙\",\n        10069 to \"萌宠\",\n\n        10070 to \"社会\",\n        10071 to \"动物\",\n        10072 to \"自然\",\n        10073 to \"医疗\",\n        10074 to \"军事\",\n        10075 to \"罪案\",\n        10076 to \"神秘\",\n        10077 to \"旅行\",\n        10078 to \"武侠\",\n        10079 to \"青春\",\n\n        10080 to \"都市\",\n        10081 to \"古装\",\n        10082 to \"谍战\",\n        10083 to \"经典\",\n        10084 to \"情感\",\n        10085 to \"神话\",\n        10086 to \"年代\",\n        10087 to \"农村\",\n        10088 to \"刑侦\",\n        10089 to \"军旅\",\n\n        10090 to \"访谈\",\n        10091 to \"脱口秀\",\n        10092 to \"真人秀\",\n        //10093\n        10094 to \"选秀\",\n        10095 to \"旅游\",\n        10096 to \"演唱会\",\n        10097 to \"亲子\",\n        10098 to \"晚会\",\n        10099 to \"养成\",\n\n        10100 to \"文化\",\n        //10101\n        10102 to \"特摄\",\n        10103 to \"短剧\",\n        10104 to \"短片\",\n    )\n\n    private val animeStyleIds = listOf(\n        -1, 10010, 10011, 10012, 10013, 10102, 10015, 10016, 10017, 10018,\n        10020, 10021, 10022, 10023, 10024, 10025, 10026, 10027, 10028, 10029,\n        10030, 10031, 10032, 10033, 10034, 10035, 10036, 10037, 10038, 10039,\n        10040, 10041, 10042, 10043, 10044, 10045, 10046, 10047, 10048\n    )\n    private val guochuangStyleIds = listOf(\n        -1, 10010, 10011, 10012, 10013, 10102, 10015, 10016, 10018, 10019,\n        10020, 10021, 10078, 10022, 10023, 10024, 10025, 10057, 10026, 10027,\n        10028, 10029, 10030, 10031, 10033, 10035, 10036, 10037, 10038, 10039,\n        10040, 10041, 10042, 10043, 10044, 10045, 10046, 10047, 10048, 10049\n    )\n    private val varietyStyleIds = listOf(\n        -1, 10040, 10090, 10091, 10092, 10094, 10045, 10095, 10098, 10096,\n        10084, 10051, 10097, 10100, 10048, 10069, 10099\n    )\n    private val movieStyleIds = listOf(\n        -1, 10104, 10050, 10051, 10052, 10053, 10054, 10023, 10055, 10056,\n        10057\n    )\n    private val tvStyleIds = listOf(\n        -1, 10021, 10018, 10058, 10078, 10079, 10103, 10080, 10081, 10082,\n        10083, 10084, 10057, 10039, 10085, 10017, 10086, 10087, 10088, 10050,\n        10061, 10033, 10089, 10023,\n    )\n    private val documentaryStyleIds = listOf(\n        -1, 10033, 10045, 10065, 10066, 10067, 10068, 10069, 10070, 10071,\n        10072, 10073, 10074, 10064, 10075, 10076, 10077, 10038, -10,\n    )\n\n    val animeStyles by lazy { animeStyleIds.associateWith { styleFilter[it]!! } }\n    val guochuangStyles by lazy { guochuangStyleIds.associateWith { styleFilter[it]!! } }\n    val varietyStyles by lazy { varietyStyleIds.associateWith { styleFilter[it]!! } }\n    val movieStyles by lazy { movieStyleIds.associateWith { styleFilter[it]!! } }\n    val tvStyles by lazy { tvStyleIds.associateWith { styleFilter[it]!! } }\n    val documentaryStyles by lazy { documentaryStyleIds.associateWith { styleFilter[it]!! } }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/index/IndexOrder.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.index\n\nenum class IndexOrder(val id: Int) {\n    UpdateTime(0), DanmakuCount(1), PlayCount(2), FollowCount(3),\n    Score(4), StartTime(5), PublishTime(6)\n}\n\nprivate val animeIds = listOf(3, 0, 4, 2, 5)\nprivate val guochuangIds = listOf(3, 0, 4, 2, 5)\nprivate val varietyIds = listOf(2, 0, 6, 4, 1)\nprivate val tvIds = listOf(2, 0, 1, 3, 4)\nprivate val movieIds = listOf(2, 0, 6, 4)\nprivate val documentaryIds = listOf(2, 4, 0, 6, 1)\n\nval animeIndexOrders by lazy { animeIds.map { IndexOrder.entries[it] } }\nval guochuangIndexOrders by lazy { guochuangIds.map { IndexOrder.entries[it] } }\nval varietyIndexOrders by lazy { varietyIds.map { IndexOrder.entries[it] } }\nval tvIndexOrders by lazy { tvIds.map { IndexOrder.entries[it] } }\nval movieIndexOrders by lazy { movieIds.map { IndexOrder.entries[it] } }\nval documentaryIndexOrders by lazy { documentaryIds.map { IndexOrder.entries[it] } }\n\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/index/IndexResult.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.index\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class IndexResultData(\n    @SerialName(\"has_next\")\n    val hasNext: Int,\n    val list: List<IndexResultItem>,\n    val num: Int,\n    val size: Int,\n    val total: Int\n) {\n    @Serializable\n    data class IndexResultItem(\n        val badge: String,\n        @SerialName(\"badge_info\")\n        val badgeInfo: BadgeInfo,\n        @SerialName(\"badge_type\")\n        val badgeType: Int,\n        val cover: String,\n        @SerialName(\"first_ep\")\n        val firstEp: FirstEp,\n        @SerialName(\"index_show\")\n        val indexShow: String,\n        @SerialName(\"is_finish\")\n        val isFinish: Int,\n        val link: String,\n        @SerialName(\"media_id\")\n        val mediaId: Int,\n        val order: String,\n        @SerialName(\"order_type\")\n        val orderType: String,\n        val score: String,\n        @SerialName(\"season_id\")\n        val seasonId: Int,\n        @SerialName(\"season_status\")\n        val seasonStatus: Int,\n        @SerialName(\"season_type\")\n        val seasonType: Int,\n        val subTitle: String,\n        val title: String,\n        @SerialName(\"title_icon\")\n        val titleIcon: String\n    ) {\n        @Serializable\n        data class BadgeInfo(\n            @SerialName(\"bg_color\")\n            val bgColor: String,\n            @SerialName(\"bg_color_night\")\n            val bgColorNight: String,\n            val text: String\n        )\n\n        @Serializable\n        data class FirstEp(\n            val cover: String,\n            @SerialName(\"ep_id\")\n            val epId: Int\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/live/HistoryDanmaku.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.live\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\nimport kotlinx.serialization.json.JsonElement\nimport kotlinx.serialization.json.int\nimport kotlinx.serialization.json.jsonPrimitive\n\n@Serializable\ndata class HistoryDanmaku(\n    //val admin:List<Any>,\n    val room: List<HistoryDanmakuItem>\n) {\n    @Serializable\n    data class HistoryDanmakuItem(\n        val text: String,\n        @SerialName(\"dm_type\")\n        val dmType: Int,\n        val uid: Long,\n        val nickname: String,\n        @SerialName(\"uname_color\")\n        val unameColor: String,\n        val timeline: String,\n        @SerialName(\"isadmin\")\n        val isAdmin: Int,\n        val vip: Int,\n        val svip: Int,\n        @SerialName(\"medal\")\n        private val _medal: List<JsonElement>,\n        @Transient\n        var medal: Medal? = null,\n        val title: List<String>,\n        @SerialName(\"user_level\")\n        val userLevel: List<JsonElement>,\n        val rank: Int,\n        @SerialName(\"teamid\")\n        val teamId: Int,\n        val rnd: Int,\n        @SerialName(\"user_title\")\n        val userTitle: String,\n        @SerialName(\"guard_level\")\n        val guardLevel: Int,\n        val bubble: Int,\n        @SerialName(\"bubble_color\")\n        val bubbleColor: String,\n        val lpl: Int,\n        @SerialName(\"yeah_space_url\")\n        val yeahSpaceUrl: String,\n        @SerialName(\"jump_to_url\")\n        val jumpToUrl: String,\n        @SerialName(\"check_info\")\n        val checkInfo: CheckInfo,\n        @SerialName(\"voice_dm_info\")\n        val voiceDmInfo: VoiceDmInfo,\n        val emoticon: Emoticon\n    ) {\n        init {\n            medal = runCatching {\n                Medal(\n                    level = _medal[0].jsonPrimitive.int,\n                    name = _medal[1].jsonPrimitive.content,\n                    up = _medal[2].jsonPrimitive.content,\n                    roomId = _medal[3].jsonPrimitive.int\n                )\n            }.getOrNull()\n        }\n    }\n}\n\n/**\n * 粉丝勋章\n *\n * 返回样例\n * ```\n *  [\n *      16,         //level\n *      \"迷你鲨\",    //name\n *      \"hufang360\",//up\n *      22739471,   //up room id\n *      12478086,\n *      \"\",\n *      0,\n *      12478086,   //medal_color_border（可能是）\n *      12478086,   //medal_color_end（可能是）\n *      12478086,   //medal_color_start（可能是）\n *      0,\n *      1,\n *      4328524\n *  ]\n * ```\n *\n * @param level 勋章等级\n * @param name 勋章名称\n * @param up 主播昵称\n * @param roomId 主播房间号\n */\ndata class Medal(\n    val level: Int,\n    val name: String,\n    val up: String,\n    val roomId: Int\n)\n\n/*\n\"medal\": [\n\t\t\t\t\t16,         //level\n\t\t\t\t\t\"迷你鲨\",    //name\n\t\t\t\t\t\"hufang360\",//up\n\t\t\t\t\t22739471,   //up room id\n\t\t\t\t\t12478086,\n\t\t\t\t\t\"\",\n\t\t\t\t\t0,\n\t\t\t\t\t12478086,   //medal_color_border\n\t\t\t\t\t12478086,   //medal_color_end\n\t\t\t\t\t12478086,   //medal_color_start\n\t\t\t\t\t0,\n\t\t\t\t\t1,\n\t\t\t\t\t4328524\n\t\t\t\t],\n */\n\n@Serializable\ndata class CheckInfo(\n    val ts: Int,\n    val ct: String\n)\n\n@Serializable\ndata class VoiceDmInfo(\n    @SerialName(\"voice_url\")\n    val voiceUrl: String,\n    @SerialName(\"file_format\")\n    val fileFormat: String,\n    val text: String,\n    @SerialName(\"file_duration\")\n    val fileDuration: Int,\n    @SerialName(\"file_id\")\n    val fileId: String\n)\n\n@Serializable\ndata class Emoticon(\n    val id: Int,\n    @SerialName(\"emoticon_unique\")\n    val emoticonUnique: String,\n    val text: String,\n    val perm: Int,\n    val url: String,\n    @SerialName(\"in_player_area\")\n    val inPlayerArea: Int,\n    @SerialName(\"bulge_display\")\n    val bulgeDisplay: Int,\n    @SerialName(\"is_dynamic\")\n    val isDynamic: Int,\n    val height: Int,\n    val width: Int\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/live/LiveDanmuInfoResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.live\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class DanmuInfoData(\n    val group: String,\n    @SerialName(\"business_id\")\n    val businessId: Int,\n    @SerialName(\"refresh_row_factor\")\n    val refreshRowFactor: Float,\n    @SerialName(\"refresh_rate\")\n    val refreshRate: Int,\n    @SerialName(\"max_delay\")\n    val maxDelay: Int,\n    val token: String,\n    @SerialName(\"host_list\")\n    val hostList: List<HostListItem> = emptyList()\n)\n\n@Serializable\ndata class HostListItem(\n    val host: String,\n    val port: Int,\n    @SerialName(\"wss_port\")\n    val wssPort: Int,\n    @SerialName(\"ws_port\")\n    val wsPort: Int\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/live/LiveEvent.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.live\n\ninterface LiveEvent\n\ndata class DanmakuEvent(\n    val content: String,\n    val mid: Long,\n    val username: String,\n    val medalName: String? = null,\n    val medalLevel: Int? = null,\n    val mode: Int = 1,              // 弹幕模式：1=滚动，4=顶部，5=底部\n    val fontSize: Int = 25,         // 字号大小\n    val color: Int = 0xFFFFFF,      // 颜色（十进制RGB整数）\n    val userLevel: Int = 0          // 用户等级 (0-60)\n) : LiveEvent\n\ndata class PopularityChangeEvent(\n    val popularity: Int,\n    val popularityText: String\n) : LiveEvent\n\ndata class OnlineRankCountEvent(\n    val count: Int\n) : LiveEvent\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/live/LiveFrame.kt",
    "content": "@file:Suppress(\"unused\", \"UNUSED_VARIABLE\")\n\npackage dev.aaa1115910.biliapi.http.entity.live\n\nimport io.ktor.utils.io.core.ByteReadPacket\nimport io.ktor.utils.io.core.buildPacket\nimport io.ktor.utils.io.core.toByteArray\nimport io.ktor.utils.io.core.writePacket\nimport kotlinx.io.Source\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.encodeToString\nimport kotlinx.serialization.json.Json\nimport java.io.ByteArrayInputStream\nimport java.io.DataInputStream\n\ninternal fun Source.readFrameHeader(): FrameHeader = FrameHeader(\n    readInt(), readShort(), readShort(), readInt(), readInt()\n)\n\n/**\n * 数据包头部\n *\n * @param totalLength 封包总大小（头部大小+正文大小）\n * @param headerLength 头部大小（一般为0x0010，16字节）\n * @param version 协议版本: 0普通包正文不使用压缩 1心跳及认证包正文不使用压缩2普通包正文使用zlib压缩 3普通包正文使用brotli压缩,解压为一个带头部的协议0普通包\n * @param type 操作码（封包类型）\n * @param sequence 保留字段不使用\n */\ndata class FrameHeader(\n    val totalLength: Int,\n    val headerLength: Short,\n    val version: Short,\n    val type: Int,\n    val sequence: Int\n) {\n    val dataLength get() = totalLength - headerLength\n    fun toBinary(): Source {\n        return buildPacket {\n            writeInt(this@FrameHeader.totalLength)\n            writeShort(headerLength)\n            writeShort(version)\n            writeInt(this@FrameHeader.type)\n            writeInt(sequence)\n        }\n    }\n}\n\nenum class FrameType(val code: Int) {\n    HeartRequest(2), HeartResponse(3), Normal(5), AuthRequest(7), AuthResponse(8)\n}\n\ninterface RequestFrame {\n    fun toBinary(): Source\n}\n\n@Serializable\ndata class AuthRequest(\n    val uid: Int = 0,\n    @SerialName(\"roomid\")\n    val roomId: Int,\n    @SerialName(\"protover\")\n    val protoVer: Int = 3,\n    val platform: String = \"web\",\n    val type: Int = 2,\n    val key: String = \"\"\n) : RequestFrame {\n    override fun toBinary(): Source {\n        val data = Json.encodeToString(this).toByteArray()\n        val header = FrameHeader(\n            totalLength = data.size + 16,\n            headerLength = 16,\n            version = 1,\n            type = FrameType.AuthRequest.code,\n            sequence = 1\n        )\n        return buildPacket {\n            this.writePacket(header.toBinary())\n            writePacket(ByteReadPacket(data))\n        }\n    }\n}\n\n@Serializable\ndata class AuthResponse(\n    val code: Int = -1\n) {\n    companion object {\n        fun parse(data: ByteArray): AuthResponse {\n            val bis = ByteArrayInputStream(data)\n            val dis = DataInputStream(bis)\n            val totalLength = dis.readInt()\n            val headerLength = dis.readUnsignedShort()\n            val version = dis.readUnsignedShort()\n            val type = dis.readInt()\n            val sequence = dis.readInt()\n\n            //TODO do some verity\n\n            val jsonString = String(dis.readBytes())\n            return Json.decodeFromString(jsonString)\n        }\n    }\n\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/live/LiveRoomPlayInfoResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.live\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class RoomPlayInfoData(\n    @SerialName(\"room_id\")\n    val roomId: Int,\n    @SerialName(\"short_id\")\n    val shortId: Int,\n    val uid: Long,\n    @SerialName(\"need_p2p\")\n    val needP2P: Int,\n    @SerialName(\"is_hidden\")\n    val isHidden: Boolean,\n    @SerialName(\"is_locked\")\n    val isLocked: Boolean,\n    @SerialName(\"is_portrait\")\n    val isPortrait: Boolean,\n    @SerialName(\"live_status\")\n    val liveStatus: Int,\n    @SerialName(\"hidden_till\")\n    val hiddenTill: Int,\n    @SerialName(\"lock_till\")\n    val lockTill: Int,\n    val encrypted: Boolean,\n    @SerialName(\"pwd_verified\")\n    val pwdVerified: Boolean,\n    @SerialName(\"live_time\")\n    val liveTime: Int,\n    @SerialName(\"room_shield\")\n    val roomShield: Int,\n    @SerialName(\"is_sp\")\n    val isSp: Int,\n    @SerialName(\"special_type\")\n    val specialType: Int,\n    val playUrl: String? = null,\n    @SerialName(\"all_special_types\")\n    val allSpecialTypes: List<Int> = emptyList()\n)\n/*\n\"data\": {\n\t\t\"play_url\": null,\n\t\t\"all_special_types\": [\n\t\t\t50\n\t\t]\n */"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/login/Captcha.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.login\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 申请 captcha 验证码结果\n *\n * @param type 验证方式 用于判断使用哪一种验证方式，目前所见只有极验 geetest：极验\n * @param token 登录 API token  与 captcha 无关，与登录接口有关\n * @param geetest 极验 captcha 数据\n * @param tencent 作用尚不明确\n */\n@Serializable\ndata class CaptchaData(\n    val type: String,\n    val token: String,\n    val geetest: Geetest,\n    val tencent: Tencent\n) {\n    /**\n     * 极验captcha数据\n     *\n     * @param gt 极验id\t一般为固定值\n     * @param challenge 极验KEY\t由B站后端产生用于人机验证\n     */\n    @Serializable\n    data class Geetest(\n        val challenge: String,\n        val gt: String\n    )\n\n    @Serializable\n    data class Tencent(\n        @SerialName(\"appid\")\n        val appId: String\n    )\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/login/qr/AppQR.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.login.qr\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AppQRDataRequest(\n    val url: String,\n    @SerialName(\"auth_code\")\n    val authCode: String\n)\n\n@Serializable\ndata class AppQRLoginData(\n    @SerialName(\"is_new\")\n    val isNew: Boolean,\n    val mid: Long,\n    @SerialName(\"access_token\")\n    val accessToken: String,\n    @SerialName(\"refresh_token\")\n    val refreshToken: String,\n    @SerialName(\"expires_in\")\n    val expiresIn: Int,\n    @SerialName(\"token_info\")\n    val tokenInfo: TokenInfo,\n    @SerialName(\"cookie_info\")\n    val cookieInfo: CookieInfo,\n    val sso: List<String> = emptyList()\n) {\n    @Serializable\n    data class TokenInfo(\n        val mid: Long,\n        @SerialName(\"expires_in\")\n        val expiresIn: Int,\n        @SerialName(\"access_token\")\n        val accessToken: String,\n        @SerialName(\"refresh_token\")\n        val refreshToken: String\n    )\n\n    @Serializable\n    data class CookieInfo(\n        val cookies: List<Cookie>,\n        val domains: List<String>\n    ) {\n        @Serializable\n        data class Cookie(\n            var name: String,\n            var value: String,\n            @SerialName(\"http_only\")\n            var httpOnly: Int,\n            val expires: Int,\n            var secure: Int\n        )\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/login/qr/WebQR.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.login.qr\n\nimport io.ktor.http.Cookie\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\n\n@Serializable\ndata class RequestWebQRData(\n    val url: String,\n    @SerialName(\"qrcode_key\")\n    val qrcodeKey: String\n)\n\n\n@Serializable\ndata class QRLoginResponse(\n    val code: Int,\n    val message: String,\n    val ttl: Int,\n    val data: WebQRLoginData,\n    @Transient\n    var cookies: List<Cookie> = emptyList()\n)\n\n@Serializable\ndata class WebQRLoginData(\n    val url: String,\n    @SerialName(\"refresh_token\")\n    val refreshToken: String,\n    val timestamp: Long,\n    val code: Int,\n    val message: String\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/login/sms/SendSmsResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.login.sms\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 发送验证码结果\n */\n@Serializable\ndata class SendSmsResponse(\n    @SerialName(\"captcha_key\")\n    val captchaKey: String,\n    @SerialName(\"recaptcha_url\")\n    val recaptchaUrl: String\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/login/sms/SmsLoginResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.login.sms\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SmsLoginResponse(\n    val status: Int,\n    val message: String,\n    val url: String,\n    @SerialName(\"token_info\")\n    val tokenInfo: TokenInfo? = null,\n    @SerialName(\"cookie_info\")\n    val cookieInfo: CookieInfo? = null,\n    val sso: List<String> = emptyList(),\n    @SerialName(\"is_new\")\n    val isNew: Boolean,\n    @SerialName(\"is_tourist\")\n    val isTourist: Boolean\n) {\n    @Serializable\n    data class TokenInfo(\n        val mid: Long,\n        @SerialName(\"expires_in\")\n        val expiresIn: Int,\n        @SerialName(\"access_token\")\n        val accessToken: String,\n        @SerialName(\"refresh_token\")\n        val refreshToken: String\n    )\n\n    @Serializable\n    data class CookieInfo(\n        val cookies: List<Cookie>,\n        val domains: List<String>\n    ) {\n        @Serializable\n        data class Cookie(\n            var name: String,\n            var value: String,\n            @SerialName(\"http_only\")\n            var httpOnly: Int,\n            val expires: Int,\n            var secure: Int,\n            @SerialName(\"same_site\")\n            val sameSite: Int\n        )\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcFeed.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.pgc\n\nimport dev.aaa1115910.biliapi.http.entity.web.Hover\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonArray\n\n@Serializable\ndata class PgcFeedData(\n    @Suppress(\"SpellCheckingInspection\")\n    var coursor: Int,\n    @SerialName(\"has_next\")\n    val hasNext: Boolean,\n    var items: List<FeedSubItem> = emptyList()\n) {\n    @Serializable\n    data class FeedSubItem(\n        val cover: String,\n        @SerialName(\"episode_id\")\n        val episodeId: Int,\n        val hover: Hover? = null,\n        val link: String? = null,\n        @SerialName(\"rank_id\")\n        val rankId: Int,\n        val rating: String? = null,\n        @SerialName(\"season_id\")\n        val seasonId: Int? = null,\n        @SerialName(\"season_type\")\n        val seasonType: Int? = null,\n        val stat: Stat? = null,\n        @SerialName(\"sub_title\")\n        val subTitle: String,\n        val text: JsonArray? = null,\n        val title: String,\n        val userStatus: UserStatus? = null\n    ) {\n        @Serializable\n        data class Stat(\n            val danmaku: Int,\n            val duration: Int,\n            val view: Long\n        )\n\n        @Serializable\n        data class UserStatus(\n            val follow: Int\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcFeedV3.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.pgc\n\nimport dev.aaa1115910.biliapi.http.entity.web.Hover\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonArray\n\n@Serializable\ndata class PgcFeedV3Data(\n    @Suppress(\"SpellCheckingInspection\")\n    var coursor: Int,\n    @SerialName(\"has_next\")\n    val hasNext: Boolean,\n    val items: List<FeedItem>\n) {\n    @Serializable\n    data class FeedItem(\n        @SerialName(\"rank_id\")\n        val rankId: Int,\n        @SerialName(\"sub_items\")\n        val subItems: List<FeedSubItem>,\n        val text: JsonArray? = null\n    ) {\n        @Serializable\n        data class FeedSubItem(\n            @SerialName(\"card_style\")\n            val cardStyle: String,\n            val cover: String,\n            @SerialName(\"episode_id\")\n            val episodeId: Int? = null,\n            val evaluate: String? = null,\n            val hover: Hover? = null,\n            val inline: Inline? = null,\n            val link: String? = null,\n            @SerialName(\"rank_id\")\n            val rankId: Int,\n            val rating: String? = null,\n            @SerialName(\"rating_count\")\n            val ratingCount: Int? = null,\n            val report: Report,\n            @SerialName(\"season_id\")\n            val seasonId: Int? = null,\n            @SerialName(\"season_type\")\n            val seasonType: Int? = null,\n            val stat: Stat? = null,\n            @SerialName(\"sub_items\")\n            val subItems: List<FeedSubItem>? = null,\n            @SerialName(\"sub_title\")\n            val subTitle: String,\n            val text: JsonArray? = null,\n            val title: String,\n            val userStatus: UserStatus? = null\n        ) {\n            @Serializable\n            data class Inline(\n                @SerialName(\"end_time\")\n                val endTime: Int? = null,\n                @SerialName(\"ep_id\")\n                val epId: Int,\n                @SerialName(\"first_ep\")\n                val firstEp: Int,\n                @SerialName(\"material_no\")\n                val materialNo: String? = null,\n                val scene: Int,\n                @SerialName(\"start_time\")\n                val startTime: Int? = null\n            )\n\n            @Serializable\n            data class Report(\n                @SerialName(\"first_ep\")\n                val firstEp: Int? = null,\n                val scene: Int? = null\n            )\n\n            @Serializable\n            data class Stat(\n                val danmaku: Int,\n                val duration: Int,\n                val view: Long\n            )\n\n            @Serializable\n            data class UserStatus(\n                val follow: Int\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/pgc/PgcWebInitialStateData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.pgc\n\nimport dev.aaa1115910.biliapi.http.entity.web.Hover\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonArray\nimport kotlinx.serialization.json.JsonElement\n\n/**\n * PGC 首页 ssr 数据\n */\n@Serializable\ndata class PgcWebInitialStateData(\n    val modules: Modules,\n) {\n    /**\n     * @param banner 轮播图\n     * @param index 索引\n     * @param ext 时间表\n     */\n    @Suppress(\"KDocUnresolvedReference\")\n    @Serializable\n    data class Modules(\n        val banner: Banner,\n        //val index: Index,\n        //val ext:Ext,\n    ) {\n        @Serializable\n        data class Banner(\n            val title: String,\n            val spmid: String,\n            val size: Int,\n            val style: String,\n            val headers: JsonArray,\n            val items: List<BannerItem>,\n            val wids: JsonArray,\n            @SerialName(\"module_id\")\n            val moduleId: Int\n        ) {\n            @Serializable\n            data class BannerItem(\n                val rating: String? = null,\n                val title: String,\n                val cover: String,\n                val link: String,\n                val evaluate: String? = null,\n                val report: JsonElement? = null,\n                val hover: Hover? = null,\n                val stat: Stat? = null,\n                val values: JsonArray? = null,\n                @SerialName(\"season_id\")\n                val seasonId: Int? = null,\n                @SerialName(\"season_type\")\n                val seasonType: Int? = null,\n                @SerialName(\"rating_count\")\n                val ratingCount: Int? = null,\n                @SerialName(\"episode_id\")\n                val episodeId: Int? = null,\n                @SerialName(\"big_cover\")\n                val bigCover: String? = null,\n                @SerialName(\"play_btn\")\n                val playBtn: Int? = null,\n                @SerialName(\"play_title\")\n                val playTitle: String? = null,\n                @SerialName(\"rank_id\")\n                val rankId: Int,\n                @SerialName(\"user_status\")\n                val userStatus: UserStatus? = null,\n                @SerialName(\"date_ts\")\n                val dateTs: Int? = null,\n                @SerialName(\"day_of_week\")\n                val dayOfWeek: Int? = null,\n                @SerialName(\"is_today\")\n                val isToday: Int? = null,\n                @SerialName(\"is_latest\")\n                val isLatest: Int? = null,\n                val id: String,\n                @SerialName(\"showReportData\")\n                val showReportData: ShowReportData,\n                // 当前获取到的 json 中未包含 webpcover 和 webpbigcover\n                //@SerialName(\"webpcover\")\n                //val webpCover: String,\n                //@SerialName(\"webpbigcover\")\n                //val webpBigCover: String\n            ) {\n\n                @Serializable\n                data class Stat(\n                    val view: Long\n                )\n\n                @Serializable\n                data class UserStatus(\n                    val follow: Int\n                )\n\n                @Serializable\n                data class ShowReportData(\n                    @SerialName(\"module_type\")\n                    val moduleType: String,\n                    @SerialName(\"module_id\")\n                    val moduleId: Int,\n                    @SerialName(\"ep_id\")\n                    val epId: Int? = null,\n                    @SerialName(\"season_id\")\n                    val seasonId: Int? = null\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/proxy/PlayUrl.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.proxy\n\nimport dev.aaa1115910.biliapi.http.entity.video.ClipInfo\nimport dev.aaa1115910.biliapi.http.entity.video.DashData\nimport dev.aaa1115910.biliapi.http.entity.video.DashFlac\nimport dev.aaa1115910.biliapi.http.entity.video.Durl\nimport dev.aaa1115910.biliapi.http.entity.video.RecordInfo\nimport dev.aaa1115910.biliapi.http.entity.video.SegmentBase\nimport dev.aaa1115910.biliapi.http.entity.video.SupportFormat\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ProxyWebPlayUrlData(\n    val code: Int = 0,\n    @SerialName(\"is_preview\")\n    val isPreview: Int = 0,\n    val fnver: Int = 0,\n    val fnval: Int = 0,\n    @SerialName(\"video_project\")\n    val videoProject: Boolean = false,\n    val type: String = \"\",\n    val bp: Int = 0,\n    @SerialName(\"vip_type\")\n    val vipType: Int = 0,\n    @SerialName(\"vip_status\")\n    val vipStatus: Int = 0,\n    @SerialName(\"is_drm\")\n    val isDrm: Boolean = false,\n    @SerialName(\"no_rexcode\")\n    val noRexcode: Int = 0,\n    @SerialName(\"has_paid\")\n    val hasPaid: Boolean = false,\n    val status: Int = 0,\n    val from: String,\n    val result: String,\n    val message: String,\n    val quality: Int,\n    val format: String,\n    @SerialName(\"timelength\")\n    val timeLength: Int,\n    @SerialName(\"accept_format\")\n    val acceptFormat: String,\n    @SerialName(\"accept_description\")\n    val acceptDescription: List<String> = emptyList(),\n    @SerialName(\"accept_quality\")\n    val acceptQuality: List<Int> = emptyList(),\n    @SerialName(\"video_codecid\")\n    val videoCodecId: Int,\n    @SerialName(\"seek_param\")\n    val seekParam: String,\n    @SerialName(\"seek_type\")\n    val seekType: String,\n    val durl: List<Durl> = emptyList(),\n    val dash: ProxyWebDash? = null,\n    @SerialName(\"support_formats\")\n    val supportFormats: List<SupportFormat> = emptyList(),\n    @SerialName(\"last_play_time\")\n    val lastPlayTime: Int = 0,\n    @SerialName(\"last_play_cid\")\n    val lastPlaycid: Long = 0,\n    @SerialName(\"clip_info_list\")\n    val clipInfoList: List<ClipInfo> = emptyList(),\n    @SerialName(\"record_info\")\n    val recordInfo: RecordInfo? = null\n)\n\n@Serializable\ndata class ProxyAppPlayUrlData(\n    val code: Int = 0,\n    @SerialName(\"is_preview\")\n    val isPreview: Int = 0,\n    val fnver: Int = 0,\n    val fnval: Int = 0,\n    @SerialName(\"video_project\")\n    val videoProject: Boolean = false,\n    val type: String = \"\",\n    val bp: Int = 0,\n    @SerialName(\"vip_type\")\n    val vipType: Int = 0,\n    @SerialName(\"vip_status\")\n    val vipStatus: Int = 0,\n    @SerialName(\"is_drm\")\n    val isDrm: Boolean = false,\n    @SerialName(\"no_rexcode\")\n    val noRexcode: Int = 0,\n    @SerialName(\"has_paid\")\n    val hasPaid: Boolean = false,\n    val status: Int = 0,\n    val from: String,\n    val result: String,\n    val message: String,\n    val quality: Int,\n    val format: String,\n    @SerialName(\"timelength\")\n    val timeLength: Int,\n    @SerialName(\"accept_format\")\n    val acceptFormat: String,\n    @SerialName(\"accept_description\")\n    val acceptDescription: List<String> = emptyList(),\n    @SerialName(\"accept_quality\")\n    val acceptQuality: List<Int> = emptyList(),\n    @SerialName(\"video_codecid\")\n    val videoCodecId: Int,\n    @SerialName(\"seek_param\")\n    val seekParam: String,\n    @SerialName(\"seek_type\")\n    val seekType: String,\n    val durl: List<Durl> = emptyList(),\n    val dash: ProxyAppDash? = null,\n    @SerialName(\"support_formats\")\n    val supportFormats: List<SupportFormat> = emptyList(),\n    @SerialName(\"last_play_time\")\n    val lastPlayTime: Int = 0,\n    @SerialName(\"last_play_cid\")\n    val lastPlaycid: Long = 0,\n    @SerialName(\"clip_info_list\")\n    val clipInfoList: List<ClipInfo> = emptyList(),\n    @SerialName(\"record_info\")\n    val recordInfo: RecordInfo? = null\n)\n\n@Serializable\ndata class ProxyWebDash(\n    val duration: Int,\n    val minBufferTime: Float,\n    val video: List<DashData> = emptyList(),\n    val audio: List<DashData>? = null,\n    val dolby: ProxyWebDashDolby = ProxyWebDashDolby(),\n    val flac: DashFlac? = null\n)\n\n@Serializable\ndata class ProxyWebDashDolby(\n    val audio: List<DashData>? = null,\n    val type: Int = 2\n)\n\n@Serializable\ndata class ProxyAppDash(\n    val duration: Int,\n    @SerialName(\"min_buffer_time\")\n    val minBufferTime: Float,\n    val video: List<ProxyAppDashData> = emptyList(),\n    val audio: List<ProxyAppDashData>? = null,\n    val dolby: ProxyAppDashDolby = ProxyAppDashDolby(),\n    val flac: DashFlac? = null\n)\n\n@Serializable\ndata class ProxyAppDashDolby(\n    val audio: List<DashData>? = null,\n    val type: String = \"\"\n)\n\n@Serializable\ndata class ProxyAppDashData(\n    val id: Int,\n    @SerialName(\"base_url\")\n    val baseUrl: String,\n    @SerialName(\"backup_url\")\n    val backupUrl: List<String> = emptyList(),\n    val bandwidth: Int,\n    @SerialName(\"mime_type\")\n    val mimeType: String,\n    val codecs: String,\n    val width: Int,\n    val height: Int,\n    @SerialName(\"frame_rate\")\n    val frameRate: String,\n    val sar: String,\n    @SerialName(\"start_with_sap\")\n    val startWithSap: Int,\n    @SerialName(\"segment_base\")\n    val segmentBase: SegmentBase,\n    @SerialName(\"codecid\")\n    val codecId: Int\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/region/RegionBanner.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.region\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class RegionBanner(\n    @SerialName(\"region_banner_list\")\n    val regionBannerList: List<UgcRegionBannerItem>\n)\n\n@Serializable\ndata class UgcRegionBannerItem(\n    val image: String,\n    val title: String,\n    @SerialName(\"sub_title\")\n    val subTitle: String,\n    val url: String,\n    val rid: Int\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/region/RegionDynamic.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.region\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 分区动态\n * @param banner 轮播图\n * @param card 卡片推荐位\n * @param cBottom 往下滚动页面加载数据使用的参数\n * @param cTop 往上滚动页面加载数据使用的参数\n * @param new 推荐内容，可看作加载分区视频列表的第一页\n */\n@Serializable\ndata class RegionDynamic(\n    val banner: Banner? = null,\n    val card: List<Card> = emptyList(),\n    @SerialName(\"cbottom\")\n    val cBottom: Long,\n    @SerialName(\"ctop\")\n    val cTop: Long,\n    val new: List<RegionDynamicList.Item>\n) {\n    @Serializable\n    data class Banner(\n        val top: List<Top>\n    ) {\n        @Serializable\n        data class Top(\n            @SerialName(\"client_ip\")\n            val clientIp: String? = null,\n            @SerialName(\"cm_mark\")\n            val cmMark: Int,\n            val hash: String,\n            val id: Int,\n            val image: String,\n            val index: Int,\n            @SerialName(\"is_ad\")\n            val isAd: Boolean? = null,\n            @SerialName(\"is_ad_loc\")\n            val isAdLoc: Boolean? = null,\n            @SerialName(\"request_id\")\n            val requestId: String,\n            @SerialName(\"resource_id\")\n            val resourceId: Int,\n            @SerialName(\"server_type\")\n            val serverType: Int,\n            @SerialName(\"src_id\")\n            val srcId: Int? = null,\n            val title: String,\n            val uri: String\n        )\n    }\n\n    @Serializable\n    data class Card(\n        val body: List<Item>,\n        @SerialName(\"card_id\")\n        val cardId: Int,\n        val title: String,\n        val type: String\n    )\n\n    @Serializable\n    data class Item(\n        val cover: String,\n        @SerialName(\"cover_left_icon_1\")\n        val coverLeftIcon1: Int? = null,\n        @SerialName(\"cover_left_text_1\")\n        val coverLeftText1: String? = null,\n        val danmaku: Int? = null,\n        val duration: Int? = null,\n        val face: String? = null,\n        val favourite: Int? = null,\n        val goto: String,\n        val like: Int? = null,\n        val name: String? = null,\n        val param: String,\n        val play: Long? = null,\n        @SerialName(\"pubdate\")\n        val pubDate: Int,\n        val reply: Int? = null,\n        val rid: Int? = null,\n        @SerialName(\"rname\")\n        val rName: String? = null,\n        val title: String,\n        val uri: String,\n        val children: List<Item>? = null\n    )\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/region/RegionDynamicList.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.region\n\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class RegionDynamicList(\n    @SerialName(\"cbottom\")\n    val cBottom: Long,\n    @SerialName(\"ctop\")\n    val cTop: Long,\n    val new: List<Item>\n) {\n    @Serializable\n    data class Item(\n        val cover: String,\n        @SerialName(\"cover_left_icon_1\")\n        val coverLeftIcon1: Int,\n        @SerialName(\"cover_left_text_1\")\n        val coverLeftText1: String,\n        val danmaku: Int? = null,\n        val duration: Int,\n        val face: String,\n        val favourite: Int? = null,\n        val goto: String,\n        val like: Int? = null,\n        val name: String,\n        val param: String,\n        val play: Long? = null,\n        @SerialName(\"pubdate\")\n        val pubDate: Int,\n        val reply: Int? = null,\n        val rid: Int,\n        @SerialName(\"rname\")\n        val rName: String,\n        val title: String,\n        val uri: String\n    )\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/region/RegionFeedRcmd.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.region\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class RegionFeedRcmd(\n    val archives: List<Archive>\n) {\n    @Serializable\n    data class Archive(\n        val aid: Long,\n        val bvid: String,\n        val cid: Long,\n        val title: String,\n        val cover: String,\n        val duration: Int,\n        val pubdate: Long,\n        val stat: Stat,\n        val author: Author,\n        val trackid: String,\n        val goto: String,\n        @SerialName(\"rec_reason\")\n        val recReason: String\n    ) {\n        @Serializable\n        data class Stat(\n            val view: Long,\n            val like: Int,\n            val danmaku: Int\n        )\n\n        @Serializable\n        data class Author(\n            val mid: Long,\n            val name: String\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/region/RegionLocs.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.region\n\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonObject\n\n@Serializable\ndata class RegionLocs(\n    @SerialName(\"ads_control\")\n    val adsControl: AdsControl,\n    val code: Int,\n    val count: Int,\n    val data: Map<String, List<LocData>>,\n    val live: JsonObject? = null,\n    val message: String\n) {\n    @Serializable\n    data class AdsControl(\n        @SerialName(\"has_danmu\")\n        val hasDanmu: Int,\n        @SerialName(\"has_live_booking_ad\")\n        val hasLiveBookingAd: Boolean,\n        @SerialName(\"under_player_scroller_seconds\")\n        val underPlayerScrollerSeconds: Int\n    )\n\n    @Serializable\n    data class LocData(\n        @SerialName(\"activity_type\")\n        val activityType: Int,\n        @SerialName(\"ad_cb\")\n        val adCb: String,\n        @SerialName(\"ad_desc\")\n        val adDesc: String,\n        @SerialName(\"adver_name\")\n        val adverName: String,\n        val agency: String,\n        val area: Int,\n        @SerialName(\"asg_id\")\n        val asgId: Int,\n        @SerialName(\"business_mark\")\n        val businessMark: JsonObject? = null,\n        @SerialName(\"card_type\")\n        val cardType: Int,\n        @SerialName(\"click_urls\")\n        val clickUrls: JsonObject? = null,\n        @SerialName(\"cm_mark\")\n        val cmMark: Int,\n        @SerialName(\"contract_id\")\n        val contractId: String,\n        @SerialName(\"creative_type\")\n        val creativeType: Int,\n        @SerialName(\"epid\")\n        val epId: Int,\n        @SerialName(\"feedback_panel\")\n        val feedbackPanel: JsonObject? = null,\n        val id: Int,\n        val inline: Inline,\n        val intro: String,\n        @SerialName(\"is_ad_loc\")\n        val isAdLoc: Boolean,\n        @SerialName(\"jump_target\")\n        val jumpTarget: Int,\n        val label: String,\n        @SerialName(\"litpic\")\n        val litPic: String,\n        val mid: String,\n        val name: String,\n        @SerialName(\"null_frame\")\n        val nullFrame: Boolean,\n        @SerialName(\"operater\")\n        val operater: String,\n        val pic: String,\n        @SerialName(\"pic_main_color\")\n        val picMainColor: String,\n        @SerialName(\"pos_num\")\n        val posNum: Int,\n        @SerialName(\"request_id\")\n        val requestId: String,\n        @SerialName(\"res_id\")\n        val resId: Int,\n        val room: JsonObject? = null,\n        @SerialName(\"sales_type\")\n        val salesType: Int,\n        val season: JsonObject? = null,\n        @SerialName(\"server_type\")\n        val serverType: Int,\n        @SerialName(\"show_urls\")\n        val showUrls: JsonObject? = null,\n        @SerialName(\"src_id\")\n        val srcId: Int,\n        @SerialName(\"stime\")\n        val sTime: Int,\n        val style: Int,\n        @SerialName(\"sub_title\")\n        val subTitle: String,\n        val title: String,\n        @SerialName(\"track_id\")\n        val trackId: String,\n        val url: String\n    ) {\n        @Serializable\n        data class Inline(\n            @SerialName(\"inline_barrage_switch\")\n            val inlineBarrageSwitch: Int,\n            @SerialName(\"inline_type\")\n            val inlineType: Int,\n            @SerialName(\"inline_url\")\n            val inlineUrl: String,\n            @SerialName(\"inline_use_same\")\n            val inlineUseSame: Int\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/reply/Comment.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.reply\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\n\n@Serializable\ndata class CommentData(\n    val assist: Int,\n    val blacklist: Int,\n    val callbacks: JsonElement? = null,\n    @SerialName(\"cm_info\")\n    val cmInfo: CmInfo? = null,\n    val config: Config,\n    val control: Control,\n    val cursor: Cursor,\n    val effects: Effects,\n    @SerialName(\"esports_grade_card\")\n    val esportsGradeCard: JsonElement? = null,\n    val note: Int,\n    val replies: List<Reply> = emptyList(),\n    val top: Top,\n    @SerialName(\"top_replies\")\n    val topReplies: JsonElement? = null,\n    @SerialName(\"up_selection\")\n    val upSelection: UpSelection? = null,\n    val upper: Upper,\n    val vote: Int\n) {\n    /**\n     * 广告控制\n     */\n    @Serializable\n    data class CmInfo(\n        val ads: JsonElement\n    )\n\n    /**\n     * 游标信息\n     */\n    @Serializable\n    data class Cursor(\n        @SerialName(\"all_count\")\n        val allCount: Int? = null,\n        @SerialName(\"is_begin\")\n        val isBegin: Boolean,\n        @SerialName(\"is_end\")\n        val isEnd: Boolean,\n        val mode: Int,\n        @SerialName(\"mode_text\")\n        val modeText: String,\n        val name: String,\n        val next: Int,\n        @SerialName(\"pagination_reply\")\n        val paginationReply: PaginationReply? = null,\n        val prev: Int,\n        @SerialName(\"session_id\")\n        val sessionId: String,\n        @SerialName(\"support_mode\")\n        val supportMode: List<Int> = emptyList()\n    ) {\n        @Serializable\n        data class PaginationReply(\n            @SerialName(\"next_offset\")\n            val nextOffset: String? = null\n        )\n    }\n\n    @Serializable\n    data class Effects(\n        val preloading: String\n    )\n\n    @Serializable\n    data class Reply(\n        val action: Int,\n        val assist: Int,\n        val attr: Long,\n        val content: Content,\n        val count: Int,\n        val ctime: Int,\n        val dialog: Long,\n        @SerialName(\"dynamic_id_str\")\n        val dynamicIdStr: String,\n        val fansgrade: Int,\n        val folder: Folder,\n        val invisible: Boolean,\n        val like: Int,\n        val member: Member,\n        val mid: Long,\n        val oid: Long,\n        val parent: Long,\n        @SerialName(\"parent_str\")\n        val parentStr: String,\n        val rcount: Int,\n        val replies: List<Reply> = emptyList(),\n        @SerialName(\"reply_control\")\n        val replyControl: ReplyControl,\n        val root: Long,\n        @SerialName(\"root_str\")\n        val rootStr: String,\n        val rpid: Long,\n        @SerialName(\"rpid_str\")\n        val rpidStr: String,\n        val state: Int,\n        val type: Long,\n        @SerialName(\"up_action\")\n        val upAction: UpAction\n    ) {\n        /**\n         * 评论内容\n         */\n        @Serializable\n        data class Content(\n            @SerialName(\"at_name_to_mid\")\n            val atNameToMid: Map<String, Long> = emptyMap(),\n            val emote: Map<String, Emote> = emptyMap(),\n            @SerialName(\"jump_url\")\n            val jumpUrl: Map<String, JumpUrl> = emptyMap(),\n            @SerialName(\"max_line\")\n            val maxLine: Int,\n            val members: List<Member> = emptyList(),\n            val message: String,\n            val pictures: List<Picture> = emptyList(),\n            @SerialName(\"picture_scale\")\n            val pictureScale: Float = 1f\n        ) {\n            /**\n             * 评论中的表情\n             */\n            @Serializable\n            data class Emote(\n                val attr: Int,\n                val id: Int,\n                @SerialName(\"jump_title\")\n                val jumpTitle: String,\n                val meta: Meta,\n                val mtime: Int,\n                @SerialName(\"package_id\")\n                val packageId: Int,\n                val state: Int,\n                val text: String,\n                val type: Int,\n                val url: String\n            ) {\n                @Serializable\n                data class Meta(\n                    val size: Int\n                )\n            }\n\n            /**\n             * 超链接跳转，例如关键词之类的\n             */\n            @Serializable\n            data class JumpUrl(\n                @SerialName(\"app_name\")\n                val appName: String,\n                @SerialName(\"app_package_name\")\n                val appPackageName: String,\n                @SerialName(\"app_url_schema\")\n                val appUrlSchema: String,\n                @SerialName(\"click_report\")\n                val clickReport: String,\n                @SerialName(\"exposure_report\")\n                val exposureReport: String,\n                val extra: Extra? = null,\n                @SerialName(\"icon_position\")\n                val iconPosition: Int,\n                @SerialName(\"is_half_screen\")\n                val isHalfScreen: Boolean,\n                @SerialName(\"match_once\")\n                val matchOnce: Boolean,\n                @SerialName(\"pc_url\")\n                val pcUrl: String,\n                @SerialName(\"prefix_icon\")\n                val prefixIcon: String,\n                val state: Int,\n                val title: String,\n                val underline: Boolean\n            ) {\n                @Serializable\n                data class Extra(\n                    @SerialName(\"goods_click_report\")\n                    val goodsClickReport: String,\n                    @SerialName(\"goods_cm_control\")\n                    val goodsCmControl: Int,\n                    @SerialName(\"goods_exposure_report\")\n                    val goodsExposureReport: String,\n                    @SerialName(\"goods_show_type\")\n                    val goodsShowType: Int,\n                    @SerialName(\"is_word_search\")\n                    val isWordSearch: Boolean\n                )\n            }\n\n            @Serializable\n            data class Picture(\n                @SerialName(\"img_src\")\n                val imgSrc: String,\n                @SerialName(\"img_width\")\n                val imgWidth: Int,\n                @SerialName(\"img_height\")\n                val imgHeight: Int,\n                @SerialName(\"img_size\")\n                val imgSize: Float,\n                @SerialName(\"top_right_icon\")\n                val topRightIcon: String? = null,\n                @SerialName(\"play_gif_thumbnail\")\n                val playGifThumbnail: Boolean? = null\n            )\n        }\n    }\n\n    /**\n     * 评论折叠信息\n     */\n    @Serializable\n    data class Folder(\n        @SerialName(\"has_folded\")\n        val hasFolded: Boolean,\n        @SerialName(\"is_folded\")\n        val isFolded: Boolean,\n        val rule: String\n    )\n\n    @Serializable\n    data class Member(\n        val avatar: String,\n        @SerialName(\"avatar_item\")\n        val avatarItem: AvatarItem? = null,\n        @SerialName(\"contract_desc\")\n        val contractDesc: String? = null,\n        @SerialName(\"face_nft_new\")\n        val faceNftNew: Int,\n        @SerialName(\"fans_detail\")\n        val fansDetail: JsonElement? = null,\n        @SerialName(\"is_contractor\")\n        val isContractor: Boolean? = null,\n        @SerialName(\"is_senior_member\")\n        val isSeniorMember: Int,\n        @SerialName(\"level_info\")\n        val levelInfo: LevelInfo,\n        val mid: String,\n        val nameplate: Nameplate,\n        @SerialName(\"nft_interaction\")\n        val nftInteraction: NftInteraction? = null,\n        @SerialName(\"official_verify\")\n        val officialVerify: OfficialVerify,\n        val pendant: Pendant,\n        val rank: String,\n        val senior: Senior,\n        val sex: String,\n        val sign: String,\n        val uname: String,\n        @SerialName(\"user_sailing\")\n        val userSailing: UserSailing? = null,\n        val vip: Vip\n    ) {\n        @Serializable\n        data class AvatarItem(\n            @SerialName(\"container_size\")\n            val containerSize: ContainerSize,\n            @SerialName(\"fallback_layers\")\n            val fallbackLayers: FallbackLayers,\n            val mid: String\n        ) {\n            @Serializable\n            data class ContainerSize(\n                val height: Double,\n                val width: Double\n            )\n        }\n\n        @Serializable\n        data class LevelInfo(\n            @SerialName(\"current_exp\")\n            val currentExp: Int,\n            @SerialName(\"current_level\")\n            val currentLevel: Int,\n            @SerialName(\"current_min\")\n            val currentMin: Int,\n            @SerialName(\"next_exp\")\n            val nextExp: Int\n        )\n\n        @Serializable\n        data class Nameplate(\n            val condition: String,\n            val image: String,\n            @SerialName(\"image_small\")\n            val imageSmall: String,\n            val level: String,\n            val name: String,\n            val nid: Int\n        )\n\n        @Serializable\n        data class NftInteraction(\n            val region: Region\n        ) {\n            @Serializable\n            data class Region(\n                val icon: String,\n                @SerialName(\"show_status\")\n                val showStatus: Int,\n                val type: Int\n            )\n        }\n\n        @Serializable\n        data class OfficialVerify(\n            val desc: String,\n            val type: Int\n        )\n\n        @Serializable\n        data class Pendant(\n            val expire: Int,\n            val image: String,\n            @SerialName(\"image_enhance\")\n            val imageEnhance: String,\n            @SerialName(\"image_enhance_frame\")\n            val imageEnhanceFrame: String,\n            val name: String,\n            val pid: Int\n        )\n\n        @Serializable\n        data class Senior(\n            val status: Int? = null\n        )\n\n        @Serializable\n        data class UserSailing(\n            val cardbg: Cardbg? = null,\n            @SerialName(\"cardbg_with_focus\")\n            val cardbgWithFocus: JsonElement? = null,\n            val pendant: Pendant? = null\n        ) {\n            @Serializable\n            data class Cardbg(\n                val fan: Fan,\n                val id: Long,\n                val image: String,\n                @SerialName(\"jump_url\")\n                val jumpUrl: String,\n                val name: String,\n                val type: String\n            ) {\n                @Serializable\n                data class Fan(\n                    val color: String,\n                    @SerialName(\"is_fan\")\n                    val isFan: Int,\n                    val name: String,\n                    @SerialName(\"num_desc\")\n                    val numDesc: String,\n                    val number: Int\n                )\n            }\n\n            @Serializable\n            data class Pendant(\n                val id: Long,\n                val image: String,\n                @SerialName(\"image_enhance\")\n                val imageEnhance: String,\n                @SerialName(\"image_enhance_frame\")\n                val imageEnhanceFrame: String,\n                @SerialName(\"jump_url\")\n                val jumpUrl: String,\n                val name: String,\n                val type: String\n            )\n        }\n\n        @Serializable\n        data class Vip(\n            val accessStatus: Int,\n            @SerialName(\"avatar_subscript\")\n            val avatarSubscript: Int,\n            val dueRemark: String,\n            val label: Label,\n            @SerialName(\"nickname_color\")\n            val nicknameColor: String,\n            val themeType: Int,\n            val vipDueDate: Long,\n            val vipStatus: Int,\n            val vipStatusWarn: String,\n            val vipType: Int\n        ) {\n            @Serializable\n            data class Label(\n                @SerialName(\"bg_color\")\n                val bgColor: String,\n                @SerialName(\"bg_style\")\n                val bgStyle: Int,\n                @SerialName(\"border_color\")\n                val borderColor: String,\n                @SerialName(\"img_label_uri_hans\")\n                val imgLabelUriHans: String,\n                @SerialName(\"img_label_uri_hans_static\")\n                val imgLabelUriHansStatic: String,\n                @SerialName(\"img_label_uri_hant\")\n                val imgLabelUriHant: String,\n                @SerialName(\"img_label_uri_hant_static\")\n                val imgLabelUriHantStatic: String,\n                @SerialName(\"label_theme\")\n                val labelTheme: String,\n                val path: String,\n                val text: String,\n                @SerialName(\"text_color\")\n                val textColor: String,\n                @SerialName(\"use_img_label\")\n                val useImgLabel: Boolean\n            )\n        }\n    }\n\n    @Serializable\n    data class ReplyControl(\n        val location: String? = null,\n        @SerialName(\"max_line\")\n        val maxLine: Int,\n        @SerialName(\"sub_reply_entry_text\")\n        val subReplyEntryText: String? = null,\n        @SerialName(\"sub_reply_title_text\")\n        val subReplyTitleText: String? = null,\n        @SerialName(\"time_desc\")\n        val timeDesc: String\n    )\n\n    @Serializable\n    data class UpAction(\n        val like: Boolean,\n        val reply: Boolean\n    )\n\n    /**\n     * 置顶\n     */\n    @Serializable\n    data class Top(\n        val admin: JsonElement? = null,\n        val upper: JsonElement? = null,\n        val vote: JsonElement? = null\n    )\n\n    @Serializable\n    data class UpSelection(\n        @SerialName(\"ignore_count\")\n        val ignoreCount: Int,\n        @SerialName(\"pending_count\")\n        val pendingCount: Int\n    )\n}\n\n/**\n * 评论区显示控制\n */\n@Serializable\ndata class Config(\n    @SerialName(\"read_only\")\n    val readOnly: Boolean,\n    @SerialName(\"show_up_flag\")\n    val showUpFlag: Boolean,\n    @SerialName(\"showtopic\")\n    val showtopic: Int\n)\n\n/**\n * 评论区输入属性\n */\n@Serializable\ndata class Control(\n    @SerialName(\"answer_guide_android_url\")\n    val answerGuideAndroidUrl: String,\n    @SerialName(\"answer_guide_icon_url\")\n    val answerGuideIconUrl: String,\n    @SerialName(\"answer_guide_ios_url\")\n    val answerGuideIosUrl: String,\n    @SerialName(\"answer_guide_text\")\n    val answerGuideText: String,\n    @SerialName(\"bg_text\")\n    val bgText: String,\n    @SerialName(\"child_input_text\")\n    val childInputText: String,\n    @SerialName(\"disable_jump_emote\")\n    val disableJumpEmote: Boolean,\n    @SerialName(\"empty_page\")\n    val emptyPage: JsonElement? = null,\n    @SerialName(\"enable_charged\")\n    val enableCharged: Boolean,\n    @SerialName(\"enable_cm_biz_helper\")\n    val enableCmBizHelper: Boolean,\n    @SerialName(\"giveup_input_text\")\n    val giveupInputText: String,\n    @SerialName(\"input_disable\")\n    val inputDisable: Boolean,\n    @SerialName(\"root_input_text\")\n    val rootInputText: String,\n    @SerialName(\"screenshot_icon_state\")\n    val screenshotIconState: Int,\n    @SerialName(\"show_text\")\n    val showText: String,\n    @SerialName(\"show_type\")\n    val showType: Int,\n    @SerialName(\"upload_picture_icon_state\")\n    val uploadPictureIconState: Int,\n    @SerialName(\"web_selection\")\n    val webSelection: Boolean\n)\n\n@Serializable\ndata class Upper(\n    val mid: Long\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/reply/CommentReplyData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.reply\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class CommentReplyData(\n    val config: Config,\n    val control: Control,\n    val page: Page,\n    val replies: List<CommentData.Reply> = emptyList(),\n    val root: CommentData.Reply,\n//    @SerialName(\"show_text\")\n//    val showText: String,\n//    @SerialName(\"show_type\")\n//    val showType: Int,\n    val upper: Upper\n) {\n    @Serializable\n    data class Page(\n        val count: Int,\n        val num: Int,\n        val size: Int\n    )\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/reply/Layers.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.reply\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\n\n@Serializable\ndata class FallbackLayers(\n    @SerialName(\"is_critical_group\")\n    val isCriticalGroup: Boolean,\n    @SerialName(\"layers\")\n    val layers: List<Layer>\n) {\n    @Serializable\n    data class Layer(\n        @SerialName(\"general_spec\")\n        val generalSpec: GeneralSpec,\n        @SerialName(\"layer_config\")\n        val layerConfig: LayerConfig,\n        val resource: Resource,\n        val visible: Boolean\n    ) {\n\n        /**\n         * @param tags AVATAR_LAYER ICON_LAYER PENDENT_LAYER\n         */\n        @Serializable\n        data class LayerConfig(\n            @SerialName(\"is_critical\")\n            val isCritical: Boolean? = null,\n            @SerialName(\"layer_mask\")\n            val layerMask: LayerMask? = null,\n            val tags: Map<String, JsonElement> = emptyMap()\n        ) {\n            @Serializable\n            data class LayerMask(\n                @SerialName(\"general_spec\")\n                val generalSpec: GeneralSpec,\n                @SerialName(\"mask_src\")\n                val maskSrc: DrawSrc\n            )\n        }\n\n        @Serializable\n        data class Resource(\n            @SerialName(\"res_animation\")\n            val resAnimation: ResAnimation? = null,\n            @SerialName(\"res_image\")\n            val resImage: ResImage? = null,\n            @SerialName(\"res_native_draw\")\n            val resNativeDraw: ResNativeDraw? = null,\n            @SerialName(\"res_type\")\n            val resType: Int\n        ) {\n            @Serializable\n            data class ResAnimation(\n                @SerialName(\"webp_src\")\n                val webpSrc: ImageSrc\n            )\n\n            @Serializable\n            data class ResImage(\n                @SerialName(\"image_src\")\n                val imageSrc: ImageSrc\n            )\n\n            @Serializable\n            data class ResNativeDraw(\n                @SerialName(\"draw_src\")\n                val drawSrc: DrawSrc\n            )\n        }\n    }\n}\n\n@Serializable\ndata class GeneralSpec(\n    @SerialName(\"pos_spec\")\n    val posSpec: PosSpec,\n    @SerialName(\"render_spec\")\n    val renderSpec: RenderSpec,\n    @SerialName(\"size_spec\")\n    val sizeSpec: SizeSpec\n) {\n    @Serializable\n    data class PosSpec(\n        @SerialName(\"axis_x\")\n        val axisX: Double,\n        @SerialName(\"axis_y\")\n        val axisY: Double,\n        @SerialName(\"coordinate_pos\")\n        val coordinatePos: Int\n    )\n\n    @Serializable\n    data class RenderSpec(\n        val opacity: Int\n    )\n\n    @Serializable\n    data class SizeSpec(\n        val height: Double,\n        val width: Double\n    )\n}\n\n@Serializable\ndata class ImageSrc(\n    val local: Int? = null,\n    @SerialName(\"placeholder\")\n    val placeholder: Int? = null,\n    val remote: Remote? = null,\n    @SerialName(\"src_type\")\n    val srcType: Int\n) {\n    @Serializable\n    data class Remote(\n        @SerialName(\"bfs_style\")\n        val bfsStyle: String,\n        val url: String? = null\n    )\n}\n\n@Serializable\ndata class DrawSrc(\n    val draw: Draw,\n    @SerialName(\"src_type\")\n    val srcType: Int\n) {\n    @Serializable\n    data class Draw(\n        @SerialName(\"color_config\")\n        val colorConfig: ColorConfig,\n        @SerialName(\"draw_type\")\n        val drawType: Int,\n        @SerialName(\"fill_mode\")\n        val fillMode: Int\n    ) {\n        @Serializable\n        data class ColorConfig(\n            val day: DayNight,\n            @SerialName(\"is_dark_mode_aware\")\n            val isDarkModeAware: Boolean = false,\n            val night: DayNight? = null\n        ) {\n            @Serializable\n            data class DayNight(\n                val argb: String\n            )\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/search/KeywordSuggest.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.search\n\nimport dev.aaa1115910.biliapi.http.entity.search.KeywordSuggest.Result\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\nimport kotlinx.serialization.json.JsonElement\n\n/**\n * @param result 当搜索词为空时为null，当有搜索建议时为[Result]，当有搜索词但无搜索建议时为[List]\n */\n@Serializable\ndata class KeywordSuggest(\n    @SerialName(\"exp_str\")\n    val expStr: String,\n    val code: Int,\n    //val cost: Cost,\n    val msg: String? = null,\n    val result: JsonElement? = null,\n    @Transient\n    val suggests: MutableList<Result.Tag> = mutableListOf(),\n    //@SerialName(\"page caches\")\n    //val pageCaches: PageCaches,\n    //val sengine: Sengine,\n    val stoken: String\n) {\n    @Serializable\n    data class Cost(\n        val about: SearchCost\n    )\n\n    @Serializable\n    data class Result(\n        val tag: List<Tag>\n    ) {\n        /**\n         * @param value 关键词内容\n         * @param term\n         * @param ref 0\n         * @param name 显示内容 在无高亮显示时与value相同 有高亮显示时带有<em class=\"suggest_high_light\">的xml标签\n         * @param spid\n         */\n        @Serializable\n        data class Tag(\n            val value: String,\n            val term: String,\n            val ref: Int,\n            val name: String,\n            val spid: Int\n        )\n    }\n\n    @Serializable\n    data class PageCaches(\n        @SerialName(\"save cache\")\n        val saveCache: String\n    )\n\n    @Serializable\n    data class Sengine(\n        val usage: Int\n    )\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/search/SearchCost.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.search\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SearchCost(\n    @SerialName(\"params_check\")\n    val paramsCheck: String,\n    @SerialName(\"is_risk_query\")\n    val isRiskQuery: String? = null,\n    @SerialName(\"illegal_handler\")\n    val illegalHandler: String? = null,\n    @SerialName(\"as_response_format\")\n    val asResponseFormat: String? = null,\n    @SerialName(\"mysql_request\")\n    val mysqlRequest: String? = null,\n    @SerialName(\"as_request\")\n    val asRequest: String? = null,\n    @SerialName(\"save_cache\")\n    val saveCache: String? = null,\n    @SerialName(\"as_request_format\")\n    val asRequestFormat: String? = null,\n    @SerialName(\"hotword_request\")\n    val hotwordRequest: String? = null,\n    @SerialName(\"hotword_request_format\")\n    val hotwordRequestFormat: String? = null,\n    @SerialName(\"hotword_response_format\")\n    val hotwordResponseFormat: String? = null,\n    @SerialName(\"deserialize_response\")\n    val deserializeResponses: String? = null,\n    val total: String,\n    @SerialName(\"main_handler\")\n    val mainHandler: String? = null,\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/search/SearchResult.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.search\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonElement\nimport kotlinx.serialization.json.decodeFromJsonElement\nimport kotlinx.serialization.json.jsonArray\nimport kotlinx.serialization.json.jsonObject\nimport kotlinx.serialization.json.jsonPrimitive\n\n/**\n * 搜索结果\n */\n@Serializable\ndata class SearchResultData(\n    val seid: String,\n    val page: Int,\n    @SerialName(\"pagesize\")\n    val pageSize: Int,\n    val numResults: Int,\n    val numPages: Int,\n    @SerialName(\"suggest_keyword\")\n    val suggestKeyword: String,\n    @SerialName(\"rqt_type\")\n    val rqtType: String,\n    @SerialName(\"cost_time\")\n    val costTime: SearchCost? = null,\n    @SerialName(\"exp_list\")\n    val expList: JsonElement? = null,\n    @SerialName(\"egg_hit\")\n    val eggHit: Int,\n    @SerialName(\"pageinfo\")\n    val pageInfo: PageInfo? = null,\n    @SerialName(\"top_tlist\")\n    val topTList: TopTList? = null,\n    @SerialName(\"show_column\")\n    val showColumn: Int,\n    @SerialName(\"show_module_list\")\n    val showModuleList: List<String>? = null,\n    @SerialName(\"app_display_option\")\n    val appDisplayOption: AppDisplayOption? = null,\n    @SerialName(\"in_black_key\")\n    val inBlackKey: Int,\n    @SerialName(\"in_white_key\")\n    val inWhiteKey: Int,\n    val result: List<JsonElement>? = mutableListOf(),\n    @Transient\n    val searchAllResults: MutableList<SearchResult<SearchResultItem>> = mutableListOf(),\n    @Transient\n    val searchTypeResults: MutableList<SearchResultItem> = mutableListOf(),\n    @SerialName(\"is_search_page_grayed\")\n    val isSearchPageGrayed: Int? = null\n) {\n    init {\n        result?.forEach { searchResultJsonElement ->\n            val searchResultJsonObject = searchResultJsonElement.jsonObject\n            var resultType = searchResultJsonObject[\"result_type\"]?.jsonPrimitive?.content\n            val json = Json {\n                coerceInputValues = true\n                ignoreUnknownKeys = true\n                prettyPrint = true\n            }\n            if (resultType != null) {\n                // 综合搜索\n                val searchResultDataJsonArray = searchResultJsonObject[\"data\"]!!.jsonArray\n                val data = when (resultType) {\n                    \"activity\" -> json.decodeFromJsonElement<List<SearchActivityResult>>(\n                        searchResultDataJsonArray\n                    )\n\n                    \"media_bangumi\", \"media_ft\" -> json.decodeFromJsonElement<List<SearchMediaResult>>(\n                        searchResultDataJsonArray\n                    )\n\n                    \"video\" -> json.decodeFromJsonElement<List<SearchVideoResult>>(\n                        searchResultDataJsonArray\n                    )\n\n                    \"live_room\" -> json.decodeFromJsonElement<List<SearchLiveRoomResult>>(\n                        searchResultDataJsonArray\n                    )\n\n                    else -> {\n                        listOf()\n                    }\n                }\n\n                val resultResult = SearchResult(\n                    resultType = searchResultJsonObject[\"result_type\"]!!.jsonPrimitive.content,\n                    data = data\n                )\n                searchAllResults.add(resultResult)\n            } else {\n                // 分类搜索\n                resultType = searchResultJsonObject[\"type\"]?.jsonPrimitive?.content\n                val data = when (resultType) {\n                    \"activity\" -> json.decodeFromJsonElement<SearchActivityResult>(\n                        searchResultJsonObject\n                    )\n\n                    \"article\" -> json.decodeFromJsonElement<SearchArticleResult>(\n                        searchResultJsonObject\n                    )\n\n                    \"bili_user\" -> json.decodeFromJsonElement<SearchBiliUserResult>(\n                        searchResultJsonObject\n                    )\n\n                    \"live_room\" -> json.decodeFromJsonElement<SearchLiveRoomResult>(\n                        searchResultJsonObject\n                    )\n\n                    \"media_bangumi\", \"media_ft\" -> json.decodeFromJsonElement<SearchMediaResult>(\n                        searchResultJsonObject\n                    )\n\n                    \"topic\" -> json.decodeFromJsonElement<SearchTopicResult>(searchResultJsonObject)\n                    \"video\" -> json.decodeFromJsonElement<SearchVideoResult>(searchResultJsonObject)\n\n                    else -> {\n                        return@forEach\n                    }\n                }\n\n                searchTypeResults.add(data)\n            }\n        }\n    }\n\n    @Serializable\n    data class PageInfo(\n        @SerialName(\"live_room\")\n        val liveRoom: PageInfoData? = null,\n        val pgc: PageInfoData? = null,\n        @SerialName(\"operation_card\")\n        val operationCard: PageInfoData? = null,\n        val tv: PageInfoData? = null,\n        val movie: PageInfoData? = null,\n        @SerialName(\"bili_user\")\n        val biliUser: PageInfoData? = null,\n        @SerialName(\"live_master\")\n        val liveMaster: PageInfoData? = null,\n        @SerialName(\"live_all\")\n        val liveAll: PageInfoData? = null,\n        val topic: PageInfoData? = null,\n        @SerialName(\"upuser\")\n        val upUser: PageInfoData? = null,\n        val live: PageInfoData? = null,\n        val video: PageInfoData? = null,\n        val user: PageInfoData? = null,\n        val bangumi: PageInfoData? = null,\n        val activity: PageInfoData? = null,\n        @SerialName(\"media_ft\")\n        val mediaFt: PageInfoData? = null,\n        val article: PageInfoData? = null,\n        @SerialName(\"media_bangumi\")\n        val mediaBangumi: PageInfoData? = null,\n        val special: PageInfoData? = null,\n        @SerialName(\"live_user\")\n        val liveUser: PageInfoData? = null\n    ) {\n        @Serializable\n        data class PageInfoData(\n            var numResults: Int,\n            val total: Int,\n            val pages: Int\n        )\n    }\n\n    @Serializable\n    data class TopTList(\n        @SerialName(\"live_room\")\n        val liveRoom: Int,\n        val pgc: Int,\n        @SerialName(\"operation_card\")\n        val operationCard: Int,\n        val tv: Int,\n        val movie: Int,\n        @SerialName(\"bili_user\")\n        val biliUser: Int,\n        @SerialName(\"live_master\")\n        val liveMaster: Int,\n        @SerialName(\"topic\")\n        val topic: Int,\n        @SerialName(\"upuser\")\n        val upUser: Int,\n        val live: Int,\n        val video: Int,\n        val user: Int,\n        val bangumi: Int,\n        val activity: Int,\n        @SerialName(\"media_ft\")\n        val mediaFt: Int,\n        val article: Int,\n        @SerialName(\"media_bangumi\")\n        val mediaBangumi: Int,\n        val special: Int,\n        val card: Int,\n        @SerialName(\"live_user\")\n        val liveUser: Int,\n    )\n\n    @Serializable\n    data class AppDisplayOption(\n        @SerialName(\"is_search_page_grayed\")\n        val isSearchPageGrayed: Int\n    )\n}\n\n@Serializable\ndata class SearchResult<T>(\n    @SerialName(\"result_type\")\n    val resultType: String,\n    val data: List<T>\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/search/SearchResultItem.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.search\n\nimport dev.aaa1115910.biliapi.http.entity.user.OfficialVerify\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\nimport kotlinx.serialization.json.JsonElement\n\n@Serializable\nsealed class SearchResultItem\n\n/**\n * 活动(activity)\n */\n@Serializable\ndata class SearchActivityResult(\n    val status: Int,\n    val author: String,\n    val url: String,\n    val title: String,\n    val cover: String,\n    val pos: Int,\n    @SerialName(\"card_type\")\n    val cardType: Int,\n    val state: Int,\n    val position: Int,\n    val corner: String,\n    @SerialName(\"card_value\")\n    val cardValue: String,\n    val type: String,\n    val id: Int,\n    val desc: String\n) : SearchResultItem()\n\n/**\n * 专栏(article)\n */\n@Serializable\ndata class SearchArticleResult(\n    @SerialName(\"pub_time\")\n    val pubTime: Int,\n    val like: Int,\n    val title: String,\n    @SerialName(\"rank_offset\")\n    val rankOffset: Int,\n    val mid: Long,\n    @SerialName(\"image_urls\")\n    val imageUrls: List<String>,\n    @SerialName(\"template_id\")\n    val templateId: Int,\n    @SerialName(\"category_id\")\n    val categoryId: Int,\n    @SerialName(\"sub_type\")\n    val subType: Int,\n    val version: String,\n    val view: Long,\n    val reply: Int,\n    @SerialName(\"rank_index\")\n    val rankIndex: Int,\n    val desc: String,\n    @SerialName(\"rank_score\")\n    val rankScore: Int? = null,\n    val type: String,\n    val id: Int,\n    @SerialName(\"category_name\")\n    val categoryName: String\n) : SearchResultItem()\n\n/**\n * 用户(bili_user)\n */\n@Serializable\ndata class SearchBiliUserResult(\n    val type: String,\n    val mid: Long,\n    val uname: String,\n    val usign: String,\n    val fans: Int,\n    val videos: Int,\n    val upic: String,\n    @SerialName(\"face_nft\")\n    val faceNft: Int,\n    @SerialName(\"face_nft_type\")\n    val faceNftType: Int,\n    @SerialName(\"verify_info\")\n    val verifyInfo: String,\n    val level: Int,\n    val gender: Int,\n    @SerialName(\"is_upuser\")\n    val isUpUser: Int,\n    @SerialName(\"is_live\")\n    val isLive: Int,\n    @SerialName(\"room_id\")\n    val roomId: Int,\n    val res: List<JsonElement>,\n    @SerialName(\"official_verify\")\n    val officialVerify: OfficialVerify,\n    @SerialName(\"hit_columns\")\n    val hitColumns: List<String>,\n    @SerialName(\"is_senior_member\")\n    val isSeniorMember: Int\n) : SearchResultItem()\n\n/**\n * 番剧(media_bangumi) 影视(mdeia_ft)\n *\n * @param type 结果类型 media_bangumi：番剧 media_ft：影视\n * @param mediaId 剧集mdid\n * @param title 剧集标题 关键字用xml标签<em class=\"keyword\">标注\n * @param orgTitle 剧集原名 关键字用xml标签<em class=\"keyword\">标注 可为空\n * @param mediaType 剧集类型 1：番剧 2：电影 3：纪录片 4：国创 5：电视剧 7：综艺\n * @param cv 声优\n * @param staff 制作组\n * @param seasonId 剧集ssid\n * @param isAvid\n * @param hitColumns 关键字匹配类型\n * @param hitEpids 关键字匹配分集标题的分集epid 多个用,分隔\n * @param seasonType 剧集类型\t1：番剧 2：电影 3：纪录片 4：国创 5：电视剧 7：综艺\n * @param seasonTypeName 剧集类型文字\n * @param selectionStyle 分集选择按钮风格 horizontal：横排式 grid：按钮式\n * @param epSize 结果匹配的分集数\n * @param url 剧集重定向url\n * @param buttonText 观看按钮文字\n * @param isFollow 是否追番 需要登录(SESSDATA) 未登录则恒为0 0：否 1：是\n * @param isSelection\n * @param eps 结果匹配的分集信息\n * @param badges 剧集标志信息\n * @param cover 剧集封面url\n * @param areas 地区\n * @param styles 风格\n * @param gotoUrl 剧集重定向url\n * @param desc 简介\n * @param pubTime 开播时间 时间戳\n * @param mediaMode\n * @param fixPubTimeStr 开播时间重写信息 优先级高于[pubTime] 可为空\n * @param mediaScore 评分信息\n * @param displayInfo 剧集标志信息\n * @param pgcSeasonId 剧集ssid\n * @param corner 角标有无 2：无 13：有\n * @param indexShow\n */\n@Serializable\ndata class SearchMediaResult(\n    val type: String,\n    @SerialName(\"media_id\")\n    val mediaId: Int,\n    val title: String,\n    @SerialName(\"org_title\")\n    val orgTitle: String,\n    @SerialName(\"media_type\")\n    val mediaType: Int,\n    val cv: String,\n    val staff: String,\n    @SerialName(\"season_id\")\n    val seasonId: Int,\n    @SerialName(\"is_avid\")\n    val isAvid: Boolean,\n    @SerialName(\"hit_columns\")\n    val hitColumns: List<String>? = null,\n    @SerialName(\"hit_epids\")\n    val hitEpids: String,\n    @SerialName(\"season_type\")\n    val seasonType: Int,\n    @SerialName(\"season_type_name\")\n    val seasonTypeName: String,\n    @SerialName(\"selection_style\")\n    val selectionStyle: String,\n    @SerialName(\"ep_size\")\n    val epSize: Int,\n    val url: String,\n    @SerialName(\"button_text\")\n    val buttonText: String,\n    @SerialName(\"is_follow\")\n    val isFollow: Int,\n    @SerialName(\"is_selection\")\n    val isSelection: Int,\n    val eps: List<SearchMediaEpisode>? = null,\n    val badges: List<Badge>? = null,\n    val cover: String,\n    val areas: String,\n    val styles: String,\n    @SerialName(\"goto_url\")\n    val gotoUrl: String,\n    val desc: String,\n    @SerialName(\"pubtime\")\n    val pubTime: Int,\n    @SerialName(\"media_mode\")\n    val mediaMode: Int,\n    @SerialName(\"fix_pubtime_str\")\n    val fixPubTimeStr: String,\n    @SerialName(\"media_score\")\n    val mediaScore: MediaScore,\n    @SerialName(\"display_info\")\n    val displayInfo: List<Badge>? = null,\n    @SerialName(\"pgc_season_id\")\n    val pgcSeasonId: Int,\n    val corner: Int,\n    @SerialName(\"index_show\")\n    val indexShow: String\n) : SearchResultItem() {\n\n    /**\n     * 分集信息\n     *\n     * @param id 分集epid\n     * @param cover 分集封面url\n     * @param title 完整标题\n     * @param url 分集重定向url\n     * @param releaseDate\n     * @param badges 分集标志\n     * @param indexTitle 短标题\n     * @param longTitle 单集标题\n     */\n    @Serializable\n    data class SearchMediaEpisode(\n        val id: Int,\n        val cover: String,\n        val title: String,\n        val url: String,\n        @SerialName(\"release_date\")\n        val releaseDate: String,\n        val badges: List<Badge>? = null,\n        @SerialName(\"index_title\")\n        val indexTitle: String,\n        @SerialName(\"long_title\")\n        val longTitle: String\n    )\n\n    /**\n     * 评分信息\n     *\n     * @param score 评分\n     * @param userCount 总计评分人数\n     */\n    @Serializable\n    data class MediaScore(\n        val score: Float,\n        @SerialName(\"user_count\")\n        val userCount: Int\n    )\n\n    /**\n     * @param text 剧集标志\n     * @param textColor 文字颜色\n     * @param textColorNight 夜间文字颜色\n     * @param bgColor 背景颜色\n     * @param bgColorNight 夜间背景颜色\n     * @param borderColor 背景颜色\n     * @param borderColorNight 夜间背景颜色\n     * @param bgStyle\n     */\n    @Serializable\n    data class Badge(\n        val text: String,\n        @SerialName(\"text_color\")\n        val textColor: String,\n        @SerialName(\"text_color_night\")\n        val textColorNight: String,\n        @SerialName(\"bg_color\")\n        val bgColor: String,\n        @SerialName(\"bg_color_night\")\n        val bgColorNight: String,\n        @SerialName(\"border_color\")\n        val borderColor: String,\n        @SerialName(\"border_color_night\")\n        val borderColorNight: String,\n        @SerialName(\"bg_style\")\n        val bgStyle: Int\n    )\n}\n\n/**\n * 话题(topic)\n *\n * @param type 结果类型 固定为topic\n * @param description 简介\n * @param pubDate 发布时间 时间戳\n * @param title 标题\n * @param favourite\n * @param hitColumns 关键字匹配类型\n * @param review\n * @param rankOffset 搜索结果排名值\n * @param cover 话题封面url\n * @param update 上传时间 时间戳\n * @param mid UP主mid\n * @param click\n * @param tpType\n * @param keyword\n * @param tpId 话题id\n * @param rankIndex\n * @param author UP主昵称\n * @param arcUrl 话题页面重定向url\n * @param rankScore 结果排序量化值\n */\n@Serializable\ndata class SearchTopicResult(\n    val description: String,\n    @SerialName(\"pubdate\")\n    val pubDate: Int,\n    val title: String,\n    val favourite: Int,\n    @SerialName(\"hit_columns\")\n    val hitColumns: List<String>,\n    val review: Int,\n    @SerialName(\"rank_offset\")\n    val rankOffset: Int,\n    val cover: String,\n    val update: Int,\n    val mid: Long,\n    val click: Int,\n    @SerialName(\"tp_type\")\n    val tpType: Int,\n    val keyword: String,\n    @SerialName(\"tp_id\")\n    val tpId: Int,\n    @SerialName(\"rank_index\")\n    val rankIndex: Int,\n    val author: String,\n    val type: String,\n    @SerialName(\"arcurl\")\n    val arcUrl: String,\n    @SerialName(\"rank_score\")\n    val rankScore: Int? = null\n) : SearchResultItem()\n\n/**\n * 视频(video)\n *\n * @param type 结果类型 固定为video\n * @param id 稿件avid\n * @param author UP主昵称\n * @param mid UP主mid\n * @param typeId 视频分区tid\n * @param typeName 视频子分区名\n * @param arcUrl 视频重定向url\n * @param aid 稿件avid\n * @param bvid 稿件bvid\n * @param title 视频标题 关键字用xml标签<em class=\"keyword\">标注\n * @param description 视频简介\n * @param arcRank\n * @param pic 视频封面url\n * @param play 视频播放量\n * @param videoReview 视频弹幕量\n * @param favorites 视频收藏数\n * @param tag 视频TAG 每项TAG用,分隔\n * @param review 视频评论数\n * @param pubDate 视频投稿时间 时间戳\n * @param sendDate 视频发布时间 时间戳\n * @param duration 视频时长 HH:MM\n * @param badgePay\n * @param hitColumns 关键字匹配类型\n * @param viewType\n * @param isPay\n * @param isUnionVideo 是否为合作视频 0：否 1：是\n * @param recTags\n * @param newRecTags\n * @param rankScore 结果排序量化值\n * @param like\n * @param upic\n * @param corner\n * @param cover\n * @param desc\n * @param url\n * @param recReason\n * @param danmaku\n * @param bizData\n * @param isChargeVideo\n */\n@Serializable\ndata class SearchVideoResult(\n    val type: String,\n    val id: Long,\n    val author: String,\n    val mid: Long,\n    @SerialName(\"typeid\")\n    val typeId: String,\n    @SerialName(\"typename\")\n    val typeName: String,\n    @SerialName(\"arcurl\")\n    val arcUrl: String,\n    val aid: Long,\n    val bvid: String,\n    val title: String,\n    val description: String,\n    @SerialName(\"arcrank\")\n    val arcRank: String? = null,\n    val pic: String,\n    val play: Long,\n    @SerialName(\"video_review\")\n    val videoReview: Int,\n    val favorites: Int,\n    val tag: String,\n    val review: Int,\n    @SerialName(\"pubdate\")\n    val pubDate: Int,\n    @SerialName(\"senddate\")\n    val sendDate: Int,\n    val duration: String,\n    @SerialName(\"badgepay\")\n    val badgePay: Boolean,\n    @SerialName(\"hit_columns\")\n    val hitColumns: List<String>,\n    @SerialName(\"view_type\")\n    val viewType: String,\n    @SerialName(\"is_pay\")\n    val isPay: Int,\n    @SerialName(\"is_union_video\")\n    val isUnionVideo: Int,\n    @SerialName(\"rec_tags\")\n    val recTags: JsonElement? = null,\n    @SerialName(\"new_rec_tags\")\n    val newRecTags: List<JsonElement>,\n    @SerialName(\"rank_score\")\n    val rankScore: Int? = null,\n    val like: Int,\n    val upic: String,\n    val corner: String,\n    val cover: String,\n    val desc: String,\n    val url: String,\n    @SerialName(\"rec_reason\")\n    val recReason: String,\n    val danmaku: Int,\n    @SerialName(\"biz_data\")\n    val bizData: JsonElement? = null,\n    @SerialName(\"is_charge_video\")\n    val isChargeVideo: Int = 0,\n    val vt: Int = 0,\n    @SerialName(\"enable_vt\")\n    private val _enableVt: Int = 0,\n    @Transient\n    val enableVt: Boolean = _enableVt == 1,\n    @SerialName(\"vt_display\")\n    val vtDisplay: String,\n    val subtitle: String,\n    @SerialName(\"episode_count_text\")\n    val episodeCountText: String,\n    @SerialName(\"release_status\")\n    val releaseStatus: Int,\n        @SerialName(\"is_intervene\")\n        val isIntervene: Int\n    ) : SearchResultItem()\n    \n    /**\n     * 直播间(live_room)\n     */\n    @Serializable\n    data class SearchLiveRoomResult(\n        val type: String,\n        val uid: Long,\n        @SerialName(\"roomid\")\n        val roomId: Long,\n        val title: String,\n        val uname: String,\n        @SerialName(\"uface\")\n        private val _uface: String,\n        val online: Int,\n        @SerialName(\"user_cover\")\n        val userCover: String,\n        val cover: String,\n        @SerialName(\"live_status\")\n        val liveStatus: Int,\n        @SerialName(\"live_time\")\n        val liveTime: String,\n        val tags: String,\n        @SerialName(\"cate_name\")\n        val cateName: String,\n        @SerialName(\"short_id\")\n        val shortId: Int? = 0,\n        @SerialName(\"area_v2_name\")\n        val areaName: String? = null,\n        @SerialName(\"area_v2_id\")\n        val areaId: Int? = 0,\n        val attributions: JsonElement? = null,\n        @SerialName(\"rank_index\")\n        val rankIndex: Int,\n        @SerialName(\"rank_score\")\n        val rankScore: Int? = null,\n        @SerialName(\"rank_offset\")\n        val rankOffset: Int,\n        @SerialName(\"hit_columns\")\n        val hitColumns: List<String>? = null,\n        @SerialName(\"is_live\")\n        val isLive: Int? = null\n    ) : SearchResultItem() {\n        val uface: String\n            get() = if (_uface.startsWith(\"//\")) \"https:$_uface\" else _uface\n    }\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/search/SearchSquare.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.search\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\n\n@Serializable\ndata class WebSearchSquareData(\n    val trending: Trending\n) {\n    @Serializable\n    data class Trending(\n        val title: String,\n        @SerialName(\"trackid\")\n        val trackId: String,\n        val list: List<Hotword>,\n        @SerialName(\"top_list\")\n        val topList: JsonElement? = null\n    )\n}\n\n/**\n * 热门关键词\n *\n * @param keyword 关键词\n * @param showName 完整关键词\n * @param icon 图标 url\n * @param uri\n * @param goto\n */\n@Serializable\ndata class Hotword(\n    val keyword: String,\n    @SerialName(\"show_name\")\n    val showName: String,\n    val icon: String,\n    val uri: String,\n    val goto: String\n)\n\n@Serializable\ndata class AppSearchSquareData(\n    val type: String,\n    val title: String,\n    val data: SquareData? = null,\n    @SerialName(\"search_ranking_meta\")\n    val searchRankingMeta: SearchRankingMeta? = null,\n    @SerialName(\"history_hotword_display\")\n    val historyHotwordDisplay: Int\n) {\n    @Serializable\n    data class SquareData(\n        @SerialName(\"trackid\")\n        val trackId: String,\n        val title: String? = null,\n        val pages: Int? = null,\n        @SerialName(\"exp_str\")\n        val expStr: String? = null,\n        val list: List<SquareDataItem> = emptyList(),\n        @SerialName(\"hotword_egg_info\")\n        val hotwordEggInfo: Int? = null\n    ) {\n        @Serializable\n        data class SquareDataItem(\n            val keyword: String? = null,\n            val status: String? = null,\n            @SerialName(\"name_type\")\n            val nameType: String? = null,\n            @SerialName(\"show_name\")\n            val showName: String? = null,\n            @SerialName(\"word_type\")\n            val wordType: Int? = null,\n            val icon: String? = null,\n            val position: Int,\n            @SerialName(\"module_id\")\n            val moduleId: Int? = null,\n            @SerialName(\"resource_id\")\n            val resourceId: Int? = null,\n            @SerialName(\"live_id\")\n            val liveId: List<Int>? = null,\n            @SerialName(\"show_live_icon\")\n            val showLiveIcon: Boolean? = null,\n            @SerialName(\"hot_id\")\n            val hotId: Int? = null,\n            @SerialName(\"stat_datas\")\n            val statDatas: StatDatas? = null,\n            val title: String? = null,\n            val param: String? = null,\n            val type: String? = null,\n            val id: Long? = null,\n            @SerialName(\"pub_time\")\n            val pubTime: String? = null,\n            @SerialName(\"is_sug_style_exp\")\n            val isSugStyleExp: Int? = null,\n            @SerialName(\"more_search_type\")\n            val moreSearchType: Int? = null,\n            @SerialName(\"share_from\")\n            val shareFrom: String? = null\n        ) {\n            @Serializable\n            data class StatDatas(\n                @SerialName(\"is_commercial\")\n                val isCommercial: Int\n            )\n        }\n    }\n\n    @Serializable\n    data class SearchRankingMeta(\n        @SerialName(\"open_search_ranking\")\n        val openSearchRanking: Boolean,\n        val text: String,\n        val link: String\n    )\n}\n\n@Serializable\ndata class SearchTendingData(\n    @SerialName(\"trackid\")\n    val trackId: String,\n    val list: List<Hotword>,\n    @SerialName(\"exp_str\")\n    val expStr: String? = null,\n    @SerialName(\"hotword_egg_info\")\n    val hotwordEggInfo: Int\n) {\n    @Serializable\n    data class Hotword(\n        val position: Int,\n        val keyword: String,\n        @SerialName(\"show_name\")\n        val showName: String,\n        @SerialName(\"word_type\")\n        val wordType: Int? = null,\n        val icon: String? = null,\n        @SerialName(\"hot_id\")\n        val hotId: Int,\n        @SerialName(\"is_commercial\")\n        val isCommercial: Int\n    )\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/season/AppSeasonData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.season\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonArray\nimport kotlinx.serialization.json.JsonElement\n\n@Serializable\ndata class AppSeasonData(\n    @SerialName(\"activity_entrance\")\n    val activityEntrance: List<ActivityEntrance> = emptyList(),\n    val actor: Actor,\n    val alias: String,\n    @SerialName(\"all_buttons\")\n    val allButtons: AllButtons,\n    @SerialName(\"all_up_infos\")\n    val allUpInfos: Map<String, UpInfo> = emptyMap(),\n    val areas: List<Area> = emptyList(),\n    val badge: String,\n    val badgeInfo: Episode.BadgeInfo? = null,\n    @SerialName(\"channel_entrance\")\n    val channelEntrance: List<ChannelEntrance> = emptyList(),\n    val cover: String,\n    val detail: String,\n    @SerialName(\"dynamic_subtitle\")\n    val dynamicSubtitle: String,\n    @SerialName(\"earphone_conf\")\n    val earphoneConf: EarphoneConf,\n    @SerialName(\"enable_vt\")\n    val enableVt: Boolean,\n    val evaluate: String,\n    @SerialName(\"icon_font\")\n    val iconFont: IconFont,\n    val link: String,\n    @SerialName(\"media_badge_info\")\n    val mediaBadgeInfo: Episode.BadgeInfo,\n    @SerialName(\"media_id\")\n    val mediaId: Int,\n    val mode: Int,\n    val modules: List<Module> = emptyList(),\n    @SerialName(\"new_ep\")\n    val newEp: NewEP,\n    @SerialName(\"new_keep_activity_material\")\n    val newKeepActivityMaterial: NewKeepActivityMaterial? = null,\n    @SerialName(\"origin_name\")\n    val originName: String? = null,\n    val payment: Payment,\n    @SerialName(\"play_strategy\")\n    val playStrategy: PlayStrategy? = null,\n    @SerialName(\"player_icon\")\n    val playerIcon: PlayerIcon? = null,\n    val premieres: JsonArray? = null,\n    @SerialName(\"producer_title\")\n    val producerTitle: String? = null,\n    val publish: Publish,\n    val rating: Rating? = null,\n    val record: String,\n    @SerialName(\"refine_cover\")\n    val refineCover: String,\n    val reserve: Reserve,\n    val right: SeasonRights? = null,\n    @SerialName(\"season_id\")\n    val seasonId: Int,\n    @SerialName(\"season_title\")\n    val seasonTitle: String,\n    val series: Series,\n    @SerialName(\"share_copy\")\n    val shareCopy: String,\n    @SerialName(\"share_url\")\n    val shareUrl: String,\n    @SerialName(\"short_link\")\n    val shortLink: String,\n    @SerialName(\"show_season_type\")\n    val showSeasonType: Int,\n    @SerialName(\"square_cover\")\n    val squareCover: String,\n    val staff: Staff,\n    val stat: Stat,\n    val status: Int,\n    val styles: List<Style> = emptyList(),\n    val subtitle: String,\n    //@SerialName(\"test_switch\")\n    //val testSwitch:TestSwitch\n    val title: String,\n    val total: Int,\n    val type: Int,\n    @SerialName(\"type_desc\")\n    val typeDesc: String,\n    @SerialName(\"type_name\")\n    val typeName: String,\n    @SerialName(\"user_status\")\n    val userStatus: UserStatus,\n    @SerialName(\"user_thumbup\")\n    val userThumbup: UserThumbup? = null\n) {\n    @Serializable\n    data class ActivityEntrance(\n        @SerialName(\"activity_cover\")\n        val activityCover: String,\n        @SerialName(\"activity_link\")\n        val activityLink: String,\n        @SerialName(\"activity_subtitle\")\n        val activitySubtitle: String,\n        @SerialName(\"activity_title\")\n        val activityTitle: String,\n        @SerialName(\"activity_type\")\n        val activityType: Int,\n        val report: Report,\n        @SerialName(\"word_tag\")\n        val wordTag: String\n    ) {\n        @Serializable\n        data class Report(\n            @SerialName(\"click_event_id\")\n            val clickEventId: String,\n            val extends: Extends,\n            @SerialName(\"show_event_id\")\n            val showEventId: String\n        ) {\n            @Serializable\n            data class Extends(\n                @SerialName(\"season_type\")\n                val seasonType: Int,\n                val link: String,\n                @SerialName(\"season_id\")\n                val seasonId: Int,\n                @SerialName(\"adsense_id\")\n                val adsenseId: Int,\n                @SerialName(\"ep_id\")\n                val epId: Int,\n                @SerialName(\"pre_wtgt_id\")\n                val preWtgtId: String,\n            )\n        }\n    }\n\n    @Serializable\n    data class Actor(\n        val info: String,\n        val title: String\n    )\n\n    @Serializable\n    data class AllButtons(\n        @SerialName(\"watch_formal\")\n        val watchFormal: String\n    )\n\n    @Serializable\n    data class UpInfo(\n        val avatar: String,\n        val follower: Int,\n        @SerialName(\"is_follow\")\n        private val _isFollow: Int,\n        val isFollow: Boolean = _isFollow == 1,\n        val mid: Long,\n        val uname: String,\n        @SerialName(\"verify_type\")\n        val verifyType: Int,\n        @SerialName(\"verify_type2\")\n        val verifyType2: Int,\n        @SerialName(\"vip_label\")\n        val vipLabel: VipLabel\n    ) {\n        /**\n         * 大会员标签\n         *\n         * 暂时还没遇到十年/百年大会员的标签\n         *\n         * @param labelTheme 无会员为“”，大会员为“vip”，年度大会员为“annual_vip”\n         * @param path\n         * @param text 无会员为“”，大会员为“大会员”，年度大会员为“年度大会员”\n         */\n        @Serializable\n        data class VipLabel(\n            @SerialName(\"label_theme\")\n            val labelTheme: String,\n            val path: String,\n            val text: String\n        )\n    }\n\n    @Serializable\n    data class Area(\n        val id: Int,\n        val name: String\n    )\n\n    @Serializable\n    data class ChannelEntrance(\n        @SerialName(\"bubble_text\")\n        val bubbleText: String,\n        val link: String,\n        val name: String,\n        @SerialName(\"tag_report\")\n        val tagReport: TagReport\n    ) {\n        @Serializable\n        data class TagReport(\n            @SerialName(\"tag_type\")\n            val tagType: String,\n            @SerialName(\"tag_type_name\")\n            val tagTypeName: String,\n            @SerialName(\"version_style\")\n            val versionStyle: String\n        )\n    }\n\n    @Serializable\n    data class EarphoneConf(\n        @SerialName(\"sp_phones\")\n        val spPhones: JsonArray\n    )\n\n    @Serializable\n    data class IconFont(\n        val name: String,\n        val text: String\n    )\n\n    /**\n     * 板块，从上到下排列，包含 Tab，选集，PV等\n     *\n     * @param data 板块内容\n     * @param id 板块id\n     * @param moduleStyle\n     * @param more\n     * @param report\n     * @param style 板块样式 season/positive/section\n     * @param title 板块标题\n     */\n    @Serializable\n    data class Module(\n        val data: ModuleData,\n        val id: Long,\n        @SerialName(\"module_style\")\n        val moduleStyle: ModuleStyle,\n        val more: String = \"\",\n        val report: Report? = null,\n        val style: String,\n        val title: String\n    ) {\n        /**\n         * 板块内容，当内容为“seasons”时仅包含seasons，当内容为“episodes”时仅包含episodes，当内容为“section”时包含除seasons以外的所有内容\n         */\n        @Serializable\n        data class ModuleData(\n            val attr: Int = 0,\n            @SerialName(\"episode_id\")\n            val episodeId: Int? = null,\n            @SerialName(\"episode_ids\")\n            val episodeIds: JsonArray? = null,\n            val seasons: List<OtherSeason> = emptyList(),\n            val episodes: List<Episode> = emptyList(),\n            val id: Int? = null,\n            val more: String? = null,\n            @SerialName(\"split_text\")\n            val splitText: String? = null,\n            val title: String? = null,\n            val type: Int? = null,\n            val type2: Int? = null\n        )\n\n        @Serializable\n        data class ModuleStyle(\n            val hidden: Int = 0,\n            val line: Int\n        )\n\n        @Serializable\n        data class Report(\n            @SerialName(\"season_id\")\n            val seasonId: String,\n            @SerialName(\"season_type\")\n            val seasonType: String,\n            @SerialName(\"sec_title\")\n            val secTitle: String,\n            @SerialName(\"section_id\")\n            val sectionId: String,\n            @SerialName(\"section_type\")\n            val sectionType: String\n        )\n    }\n\n    @Serializable\n    data class NewKeepActivityMaterial(\n        val activityId: Int\n    )\n\n    @Serializable\n    data class Payment(\n        val dialog: JsonElement,\n        @SerialName(\"pay_tip\")\n        val payTip: JsonElement? = null,\n        @SerialName(\"pay_type\")\n        val payType: PayType,\n        val price: String,\n        @SerialName(\"report_type\")\n        val reportType: Int,\n        @SerialName(\"tv_price\")\n        val tvPrice: String,\n        @SerialName(\"vip_discount_price\")\n        val vipDiscountPrice: String,\n        @SerialName(\"vip_promotion\")\n        val vipPromotion: String\n    ) {\n        @Serializable\n        data class PayType(\n            @SerialName(\"allow_ticket\")\n            val allowTicket: Int\n        )\n    }\n\n    @Serializable\n    data class PlayStrategy(\n        @SerialName(\"auto_play_toast\")\n        val autoPlayToast: String,\n        @SerialName(\"recommend_show_strategy\")\n        val recommendShowStrategy: Int,\n        val strategies: List<String> = emptyList()\n    )\n\n    @Serializable\n    data class PlayerIcon(\n        val ctime: Int,\n        @SerialName(\"drag_data\")\n        val dragData: JsonElement? = null,\n        val hash1: String? = null,\n        val hash2: String? = null,\n        @SerialName(\"no_drag_data\")\n        val noDragData: JsonElement? = null,\n        val url1: String? = null,\n        val url2: String? = null\n    )\n\n    @Serializable\n    data class Reserve(\n        val episodes: JsonElement,\n        val tip: String\n    )\n\n    @Serializable\n    data class Series(\n        @SerialName(\"display_type\")\n        val displayType: Int,\n        @SerialName(\"series_id\")\n        val seriesId: Int,\n        @SerialName(\"series_title\")\n        val seriesTitle: String\n    )\n\n    @Serializable\n    data class Staff(\n        val info: String,\n        val title: String\n    )\n\n    @Serializable\n    data class Stat(\n        val coin: Int? = null,\n        val danmaku: Int? = null,\n        val favorite: Int,\n        val favorites: Int,\n        val followers: String,\n        val likes: Int,\n        val play: String,\n        val reply: Int,\n        val share: Int,\n        val views: Long,\n        val vt: Int\n    )\n\n    /**\n     * 剧集类别，例如“游戏改”、“战斗”,\"奇幻\",\"热血\"\n     */\n    @Serializable\n    data class Style(\n        val id: Int,\n        val name: String,\n        val url: String? = null\n    )\n\n    /**\n     * 用户信息\n     *\n     * @param follow 是否追剧\n     * @param followBubble\n     * @param followStatus\n     * @param pay\n     * @param pay_for\n     * @param progress 上次播放进度，仅登录时存在\n     * @param sponsor\n     * @param vipInfo 用户会员信息，仅登录时存在\n     */\n    @Serializable\n    data class UserStatus(\n        val follow: Int,\n        @SerialName(\"follow_bubble\")\n        val followBubble: Int,\n        @SerialName(\"follow_status\")\n        val followStatus: Int,\n        val pay: Int,\n        @SerialName(\"pay_for\")\n        val payFor: Int,\n        val progress: Progress? = null,\n        val review: Review? = null,\n        val sponsor: Int,\n        val vip: Int,\n        @SerialName(\"vip_frozen\")\n        val vipFrozen: Int\n    ) {\n        /**\n         * 上次播放进度\n         *\n         * @param lastEpId 上次播放剧集epid\n         * @param lastEpIndex 上次播放剧集标题 [Episode.title]\n         * @param lastTime 上次播放时间\n         */\n        @Serializable\n        data class Progress(\n            @SerialName(\"last_ep_id\")\n            val lastEpId: Int,\n            @SerialName(\"last_ep_index\")\n            val lastEpIndex: String,\n            @SerialName(\"last_time\")\n            val lastTime: Int\n        )\n\n        @Serializable\n        data class Review(\n            @SerialName(\"article_url\")\n            val articleUrl: String,\n            @SerialName(\"is_open\")\n            val isOpen: Int,\n            val score: Int\n        )\n    }\n\n    @Serializable\n    data class UserThumbup(\n        @SerialName(\"url_image_ani\")\n        val urlImageAni: String,\n        @SerialName(\"url_image_ani_cut\")\n        val urlImageAniCut: String,\n        @SerialName(\"url_image_bright\")\n        val urlImageBright: String,\n        @SerialName(\"url_image_dim\")\n        val urlImageDim: String,\n    )\n}\n\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/season/Episode.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.season\n\nimport dev.aaa1115910.biliapi.http.entity.video.Dimension\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 剧集项\n *\n * @param aid 单集稿件avid\n * @param badge 标签文字 例如会员、限免等\n * @param badgeInfo\n * @param badgeType\n * @param bvid 单集稿件bvid\n * @param cid 视频cid\n * @param cover 单集封面url\n * @param dimension 分辨率信息\n * @param duration 时长\n * @param enableVt\n * @param epId 同 [id]\n * @param from\n * @param id 单集epid\n * @param isViewHide 是否隐藏\n * @param link 单集网页url\n * @param longTitle 单集完整标题\n * @param pubTime 发布时间 时间戳\n * @param pv 0 作用尚不明确\n * @param releaseDate 空 作用尚不明确\n * @param report 仅 App 端\n * @param rights\n * @param shareCopy 《{标题}》+第n话+ 单集完整标题｝\n * @param shareUrl 单集网页url\n * @param shortLink 单集网页url短链接\n * @param skip 跳过片头片尾数据\n * @param stat 视频数据信息，例如播放数、弹幕数等\n * @param statForUnity 视频数据信息，例如播放数、弹幕数等\n * @param status\n * @param subtitle 单集副标题 观看次数文字\n * @param title 单集标题\n * @param vid 单集vid vupload_+{cid}\n */\n@Serializable\ndata class Episode(\n    val aid: Long,\n    val badge: String,\n    @SerialName(\"badge_info\")\n    val badgeInfo: BadgeInfo,\n    @SerialName(\"badge_type\")\n    val badgeType: Int = 0,\n    val bvid: String = \"\",\n    val cid: Long,\n    val cover: String,\n    val dimension: Dimension? = null,\n    val duration: Int = 0,\n    @SerialName(\"enable_vt\")\n    val enableVt: Boolean,\n    @SerialName(\"ep_id\")\n    val epId: Int = 0,\n    val from: String = \"\",\n    val id: Int,\n    @SerialName(\"is_view_hide\")\n    val isViewHide: Boolean,\n    val link: String,\n    @SerialName(\"long_title\")\n    val longTitle: String = \"\",\n    @SerialName(\"pub_time\")\n    val pubTime: Long,\n    val pv: Int,\n    @SerialName(\"release_date\")\n    val releaseDate: String = \"\",\n    val report: Report? = null,\n    val rights: EpisodeRights? = null,\n    @SerialName(\"share_copy\")\n    val shareCopy: String = \"\",\n    @SerialName(\"share_url\")\n    val shareUrl: String = \"\",\n    @SerialName(\"short_link\")\n    val shortLink: String = \"\",\n    val skip: Skip? = null,\n    val stat: Stat? = null,\n    @SerialName(\"stat_for_unity\")\n    val statForUnity: StatForUnity? = null,\n    val status: Int,\n    val subtitle: String = \"\",\n    val title: String,\n    val vid: String = \"\"\n) {\n    /**\n     * 标签\n     *\n     * @param bgColor\n     * @param bgColorNight\n     * @param text\n     */\n    @Serializable\n    data class BadgeInfo(\n        @SerialName(\"bg_color\")\n        val bgColor: String,\n        @SerialName(\"bg_color_night\")\n        val bgColorNight: String,\n        val text: String\n    )\n\n    /**\n     * 剧集版权\n     *\n     * @param allowDemand\n     * @param allowDm\n     * @param allowDownload\n     * @param areaLimit\n     */\n    @Serializable\n    data class EpisodeRights(\n        //@SerialName(\"allow_demand\")\n        //val allowDemand: Int,\n        @SerialName(\"allow_dm\")\n        val allowDm: Int,\n        @SerialName(\"allow_download\")\n        val allowDownload: Int,\n        @SerialName(\"area_limit\")\n        val areaLimit: Int\n    ) {\n        //val isAllowDemand = allowDemand == 1\n        val isAllowDm = allowDm == 1\n        val isAllowDownload = allowDownload == 1\n        val usAreaLimit = areaLimit == 1\n    }\n\n    /**\n     * 跳过片头片尾\n     *\n     * @param op OP时间\n     * @param ed ED时间\n     */\n    @Serializable\n    data class Skip(\n        val op: SkipTime,\n        val ed: SkipTime\n    ) {\n        /**\n         * 跳过时间\n         *\n         * @param start 开始时间\n         * @param end 结束时间\n         */\n        @Serializable\n        data class SkipTime(\n            val start: Int,\n            val end: Int\n        )\n    }\n\n    @Serializable\n    data class Report(\n        val aid: String,\n        @SerialName(\"ep_title\")\n        val epTitle: String,\n        val epid: String? = null,\n        val position: String,\n        @SerialName(\"season_id\")\n        val seasonId: String,\n        @SerialName(\"season_type\")\n        val seasonType: String,\n        @SerialName(\"section_id\")\n        val sectionId: String,\n        @SerialName(\"section_type\")\n        val sectionType: String,\n        val style: String? = null\n    )\n\n    @Serializable\n    data class Stat(\n        val coin: Int,\n        val danmakus: Int,\n        val likes: Int,\n        val play: Long,\n        val reply: Int,\n        val vt: Int\n    )\n\n    @Serializable\n    data class StatForUnity(\n        val coin: Int,\n        val danmaku: Danmaku,\n        val likes: Int,\n        val reply: Int,\n        val vt: Vt\n    ) {\n        @Serializable\n        data class Danmaku(\n            val icon: String,\n            @SerialName(\"pure_text\")\n            val pureText: String,\n            val text: String,\n            val value: Int\n        )\n\n        @Serializable\n        data class Vt(\n            val icon: String,\n            val text: String,\n            val value: Int\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/season/Follow.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.season\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SeasonFollowData(\n    val fmid: Int,\n    val relation: Boolean,\n    val status: Int,\n    val toast: String\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/season/SeasonSection.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.season\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonArray\n\n/**\n * 板块 花絮、PV、番外等非正片内容\n *\n * @param attr\n * @param episodeId\n * @param episodeIds\n * @param episodes 板块内容\n * @param id 板块id\n * @param title 板块标题\n * @param type\n */\n@Serializable\ndata class SeasonSection(\n    val attr: Int,\n    @SerialName(\"episode_id\")\n    val episodeId: Int,\n    @SerialName(\"episode_ids\")\n    val episodeIds: JsonArray? = null,\n    val episodes: List<Episode> = emptyList(),\n    val id: Long,\n    val title: String,\n    val type: Int\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/season/WebFollowingSeason.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.season\n\nimport dev.aaa1115910.biliapi.http.entity.video.VideoStat\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonArray\nimport kotlinx.serialization.json.JsonElement\n\n/**\n * 追剧\n */\n@Serializable\ndata class FollowingSeasonWebData(\n    val list: List<WebFollowingSeason>,\n    @SerialName(\"pn\")\n    val pageNumber: Int,\n    @SerialName(\"ps\")\n    val pageSize: Int,\n    val total: Int\n)\n\n@Serializable\ndata class FollowingSeasonAppData(\n    @SerialName(\"follow_list\")\n    val followList: List<AppFollowingSeason> = emptyList(),\n    @SerialName(\"has_next\")\n    private val _hasNext: Int,\n    val hasNext: Boolean = _hasNext == 1,\n    val series: JsonElement? = null,\n    val total: Int,\n    @SerialName(\"vip_tip\")\n    val vipTip: JsonArray? = null,\n    val want: JsonArray? = null,\n    val watched: JsonArray? = null\n)\n\n@Serializable\ndata class WebFollowingSeason(\n    val badge: String,\n    @SerialName(\"badge_ep\")\n    val badgeEp: String,\n    @SerialName(\"badge_info\")\n    val badgeInfo: BadgeInfo,\n    @SerialName(\"badge_infos\")\n    val badgeInfos: BadgeInfos? = null,\n    @SerialName(\"badge_type\")\n    val badgeType: Int,\n    @SerialName(\"both_follow\")\n    val bothFollow: Boolean,\n    @SerialName(\"can_watch\")\n    val canWatch: Int,\n    val cover: String,\n    val evaluate: String,\n    @SerialName(\"first_ep\")\n    val firstEp: Int,\n    @SerialName(\"first_ep_info\")\n    val firstEpInfo: EpInfo,\n    @SerialName(\"follow_status\")\n    val followStatus: Int,\n    @SerialName(\"formal_ep_count\")\n    val formalEpCount: Int? = null,\n    @SerialName(\"horizontal_cover_16_10\")\n    val horizontalCover1610: String? = null,\n    @SerialName(\"horizontal_cover_16_9\")\n    val horizontalCover169: String? = null,\n    @SerialName(\"is_finish\")\n    val isFinish: Int,\n    @SerialName(\"is_new\")\n    val isNew: Int,\n    @SerialName(\"is_play\")\n    val isPlay: Int,\n    @SerialName(\"is_started\")\n    val isStarted: Int,\n    @SerialName(\"media_attr\")\n    val mediaAttr: Int,\n    @SerialName(\"media_id\")\n    val mediaId: Int,\n    val mode: Int,\n    @SerialName(\"new_ep\")\n    val newEp: EpInfo,\n    val producers: List<Producer> = emptyList(),\n    val progress: String,\n    val publish: Publish,\n    @SerialName(\"renewal_time\")\n    val renewalTime: String? = null,\n    val rights: Rights,\n    @SerialName(\"season_attr\")\n    val seasonAttr: Int,\n    @SerialName(\"season_id\")\n    val seasonId: Int,\n    @SerialName(\"season_status\")\n    val seasonStatus: Int,\n    @SerialName(\"season_title\")\n    val seasonTitle: String,\n    @SerialName(\"season_type\")\n    val seasonType: Int,\n    @SerialName(\"season_type_name\")\n    val seasonTypeName: String,\n    @SerialName(\"season_version\")\n    val seasonVersion: String? = null,\n    val section: List<Section>,\n    val series: Series? = null,\n    @SerialName(\"short_url\")\n    val shortUrl: String,\n    @SerialName(\"square_cover\")\n    val squareCover: String,\n    val stat: VideoStat,\n    val styles: List<String> = emptyList(),\n    val subtitle: String,\n    @SerialName(\"subtitle_14\")\n    val subtitle14: String? = null,\n    val summary: String,\n    val title: String,\n    @SerialName(\"total_count\")\n    val totalCount: Int,\n    val url: String,\n    @SerialName(\"viewable_crowd_type\")\n    val viewableCrowdType: Int? = null\n) {\n    @Serializable\n    data class BadgeInfo(\n        @SerialName(\"bg_color\")\n        val bgColor: String,\n        @SerialName(\"bg_color_night\")\n        val bgColorNight: String,\n        val img: String? = null,\n        @SerialName(\"multi_img\")\n        val multiImg: MultiImg,\n        val text: String? = null,\n    ) {\n        @Serializable\n        data class MultiImg(\n            val color: String,\n            @SerialName(\"medium_remind\")\n            val mediumRemind: String\n        )\n    }\n\n    @Serializable\n    data class BadgeInfos(\n        @SerialName(\"vip_or_pay\")\n        val vipOrPay: BadgeInfo? = null\n    )\n\n    @Serializable\n    data class EpInfo(\n        val cover: String? = null,\n        val duration: Int? = null,\n        val id: Int? = null,\n        @SerialName(\"index_show\")\n        val indexShow: String? = null,\n        @SerialName(\"long_title\")\n        val longTitle: String? = null,\n        @SerialName(\"pub_time\")\n        val pubTime: String? = null,\n        val title: String? = null\n    )\n\n    @Serializable\n    data class Producer(\n        @SerialName(\"is_contribute\")\n        val isContribute: Int? = null,\n        val mid: Long,\n        val type: Int\n    )\n\n    @Serializable\n    data class Publish(\n        @SerialName(\"pub_time\")\n        val pubTime: String,\n        @SerialName(\"pub_time_show\")\n        val pubTimeShow: String,\n        @SerialName(\"release_date\")\n        val releaseDate: String,\n        @SerialName(\"release_date_show\")\n        val releaseDateShow: String\n    )\n\n    @Serializable\n    data class Rights(\n        @SerialName(\"demand_end_time\")\n        val demandEndTime: JsonElement? = null,\n        @SerialName(\"is_selection\")\n        val isSelection: Int,\n        @SerialName(\"selection_style\")\n        val selectionStyle: Int\n    )\n\n    @Serializable\n    data class Section(\n        @SerialName(\"ban_area_show\")\n        val banAreaShow: Int? = null,\n        val copyright: String,\n        @SerialName(\"episode_ids\")\n        val episodeIds: List<Int>,\n        @SerialName(\"limit_group\")\n        val limitGroup: Int,\n        @SerialName(\"season_id\")\n        val seasonId: Int,\n        @SerialName(\"section_id\")\n        val sectionId: Int,\n        @SerialName(\"watch_platform\")\n        val watchPlatform: Int\n    )\n\n    @Serializable\n    data class Series(\n        @SerialName(\"new_season_id\")\n        val newSeasonId: Int? = null,\n        @SerialName(\"season_count\")\n        val seasonCount: Int? = null,\n        @SerialName(\"series_id\")\n        val seriesId: Int? = null,\n        @SerialName(\"series_ord\")\n        val seriesOrd: Int? = null,\n        val title: String? = null\n    )\n}\n\n@Serializable\ndata class AppFollowingSeason(\n    val areas: List<Area> = emptyList(),\n    val badge: String,\n    @SerialName(\"badge_info\")\n    val badgeInfo: BadgeInfo,\n    @SerialName(\"badge_type\")\n    val badgeType: Int,\n    @SerialName(\"can_watch\")\n    val canWatch: Int,\n    val cover: String,\n    val follow: Int,\n    @SerialName(\"is_finish\")\n    private val _isFinish: Int,\n    val isFinish: Boolean = _isFinish == 1,\n    val movable: Int,\n    val mtime: Int,\n    @SerialName(\"new_ep\")\n    val newEp: NewEp,\n    val progress: Progress? = null,\n    @SerialName(\"season_id\")\n    val seasonId: Int,\n    @SerialName(\"season_type\")\n    val seasonType: Int,\n    @SerialName(\"season_type_name\")\n    val seasonTypeName: String,\n    val series: Series,\n    @SerialName(\"square_cover\")\n    val squareCover: String,\n    val title: String,\n    val url: String\n) {\n    @Serializable\n    data class Area(\n        val id: Int,\n        val name: String\n    )\n\n    @Serializable\n    data class BadgeInfo(\n        @SerialName(\"bg_color\")\n        val bgColor: String,\n        @SerialName(\"bg_color_night\")\n        val bgColorNight: String,\n        val img: String? = null,\n        val text: String\n    )\n\n    @Serializable\n    data class NewEp(\n        val cover: String,\n        val duration: Int,\n        val id: Int,\n        @SerialName(\"index_show\")\n        val indexShow: String,\n        @SerialName(\"is_new\")\n        private val _isNew: Int,\n        val isNew: Boolean = _isNew == 1\n    )\n\n    @Serializable\n    data class Progress(\n        @SerialName(\"index_show\")\n        val indexShow: String,\n        @SerialName(\"last_ep_id\")\n        val lastEpId: Int,\n        @SerialName(\"last_time\")\n        val lastTime: Int,\n    )\n\n    @Serializable\n    data class Series(\n        val count: Int,\n        val id: Int,\n        val title: String\n    )\n}\n\n/*\nenum class FollowingSeasonType(val id: Int) {\n    Bangumi(id = 1), FilmAndTelevision(id = 2)\n}\n\nenum class FollowingSeasonStatus(val id: Int) {\n    All(id = 0), Want(id = 1), Watching(id = 2), Watched(id = 3)\n}\n*/\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/season/WebSeasonData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.season\n\nimport dev.aaa1115910.biliapi.http.entity.user.Pendant\nimport dev.aaa1115910.biliapi.http.entity.user.Vip\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 剧集信息\n *\n * @param activity 参与的活动\n * @param alias\n * @param bkgCover 网页背景图片url 无则为空\n * @param cover 剧集封面图片url\n * @param episodes 正片剧集列表\n * @param evaluate 简介\n * @param jpTitle 空 作用尚不明确\n * @param link 简介页面url\n * @param mediaId 剧集mdid\n * @param mode 2 作用尚不明确\n * @param newEp obj\t更新信息\n * @param payment 会员&付费信息 若无相关内容则无此项\n * @param positive\n * @param publish 发布信息\n * @param rating 评分信息 若无相关内容则无此项\n * @param record 备案号 无则为空\n * @param rights 属性标志信息\n * @param seasonId 番剧ssid\n * @param seasonTitle 剧集标题\n * @param seasons 同系列所有季信息\n * @param section 花絮、PV、番外等非正片内容 若无相关内容则无此项\n * @param series 系列信息\n * @param shareCopy 《{标题}》+{备注}\n * @param shareSubTitle 备注\n * @param shareUrl 番剧播放页面url\n * @param show 网页全屏标志\n * @param squareCover 方形封面图片url\n * @param stat 状态数\n * @param status\n * @param style 剧集风格\n * @param subtitle 剧集副标题\n * @param title 剧集标题\n * @param total 总计正片集数 未完结：大多为-1 已完结：正整数\n * @param type 剧集类型 1：番剧 2：电影 3：纪录片 4：国创 5：电视剧 7：综艺\n * @param upInfo UP主信息 若无相关内容则无此项\n * @param userStatus 用户信息\n */\n@Serializable\ndata class WebSeasonData(\n    val activity: Activity,\n    val alias: String,\n    @SerialName(\"bkg_cover\")\n    val bkgCover: String,\n    val cover: String,\n    val episodes: List<Episode> = emptyList(),\n    val evaluate: String,\n    @SerialName(\"jp_title\")\n    val jpTitle: String,\n    val link: String,\n    @SerialName(\"media_id\")\n    val mediaId: Int,\n    val mode: Int,\n    @SerialName(\"new_ep\")\n    val newEp: NewEP,\n    val payment: Payment? = null,\n    val positive: Positive,\n    val publish: Publish,\n    val rating: Rating? = null,\n    val record: String,\n    val rights: SeasonRights,\n    @SerialName(\"season_id\")\n    val seasonId: Int,\n    @SerialName(\"season_title\")\n    val seasonTitle: String,\n    val seasons: List<OtherSeason> = emptyList(),\n    val section: List<SeasonSection> = emptyList(),\n    val series: Series,\n    @SerialName(\"share_copy\")\n    val shareCopy: String,\n    @SerialName(\"share_sub_title\")\n    val shareSubTitle: String,\n    @SerialName(\"share_url\")\n    val shareUrl: String,\n    val show: Show,\n    @SerialName(\"square_cover\")\n    val squareCover: String,\n    val stat: SeasonStat,\n    val status: Int,\n    val styles: List<String> = emptyList(),\n    val subtitle: String,\n    val title: String,\n    val total: Int,\n    val type: Int,\n    @SerialName(\"up_info\")\n    val upInfo: UpInfo? = null,\n    @SerialName(\"user_status\")\n    var userStatus: UserStatus\n\n) {\n    /**\n     * 当前推广活动\n     *\n     * @param headBgUrl\n     * @param id 活动id\n     * @param title 活动标题\n     */\n    @Serializable\n    data class Activity(\n        @SerialName(\"head_bg_url\")\n        val headBgUrl: String,\n        val id: Int,\n        val title: String\n    )\n\n    /**\n     * 会员&付费信息\n     *\n     * @param discount 当前折扣 例如 100 代表 100% 不打折\n     * @param payType\n     * @param price 价格 例如 5.0\n     * @param promotion\n     * @param tip\n     * @param vipDiscount 会员折扣 例如 50 代表 50%\n     * @param vipFirstPromotion\n     * @param vipPromotion\n     */\n    @Serializable\n    data class Payment(\n        val discount: Int,\n        @SerialName(\"pay_type\")\n        val payType: PayType,\n        val price: String,\n        val promotion: String,\n        val tip: String? = \"\",\n        @SerialName(\"vip_discount\")\n        val vipDiscount: Int,\n        @SerialName(\"vip_first_promotion\")\n        val vipFirstPromotion: String,\n        @SerialName(\"vip_promotion\")\n        val vipPromotion: String\n    ) {\n        /**\n         * 支付方式\n         *\n         * @param allowDiscount 允许使用折扣\n         * @param allowPack\n         * @param allowTicket\n         * @param allowTimeLimit\n         * @param allowVipDiscount 允许使用会员折扣\n         * @param forbidBb 拒绝使用 B 币支付\n         */\n        @Serializable\n        data class PayType(\n            @SerialName(\"allow_discount\")\n            val allowDiscount: Int,\n            @SerialName(\"allow_pack\")\n            val allowPack: Int,\n            @SerialName(\"allow_ticket\")\n            val allowTicket: Int,\n            @SerialName(\"allow_time_limit\")\n            val allowTimeLimit: Int,\n            @SerialName(\"allow_vip_discount\")\n            val allowVipDiscount: Int,\n            @SerialName(\"forbid_bb\")\n            val forbidBb: Int\n        )\n    }\n\n    /**\n     * @param id\n     * @param title\n     */\n    @Serializable\n    data class Positive(\n        val id: Int,\n        val title: String\n    )\n\n    /**\n     * 系列信息\n     *\n     * @param displayType\n     * @param seriesId 系列id\n     * @param seriesTitle 系列名\n     */\n    @Serializable\n    data class Series(\n        @SerialName(\"display_type\")\n        val displayType: Int,\n        @SerialName(\"series_id\")\n        val seriesId: Int,\n        @SerialName(\"series_title\")\n        val seriesTitle: String\n    )\n\n    /**\n     * 网页全屏标志\n     *\n     * @param wideScreen 是否全屏\t0：正常  1：全屏\n     */\n    @Serializable\n    data class Show(\n        @SerialName(\"wide_screen\")\n        val wideScreen: Int\n    )\n\n    /**\n     * 剧集状态数\n     *\n     * @param coins 投币数\n     * @param danmakus 弹幕数\n     * @param favorites 收藏数\n     * @param likes 点赞数\n     * @param reply 评论数\n     * @param share 分享数\n     * @param views 播放数\n     */\n    @Serializable\n    data class SeasonStat(\n        val coins: Int,\n        val danmakus: Int,\n        val favorites: Int,\n        val likes: Int,\n        val reply: Int,\n        val share: Int,\n        val views: Long\n    )\n\n    /**\n     * UP主信息\n     *\n     * @param avatar 头像图片url\n     * @param avatarSubscriptUrl\n     * @param follower 粉丝数\n     * @param isFollow 0\n     * @param mid UP主mid\n     * @param nicknameColor\n     * @param pendant  头像框\n     * @param themeType 0\n     * @param uname UP主昵称\n     * @param verifyType\n     * @param vipLabel\n     * @param vipStatus\n     * @param vipType\n     */\n    @Serializable\n    data class UpInfo(\n        val avatar: String,\n        @SerialName(\"avatar_subscript_url\")\n        val avatarSubscriptUrl: String,\n        val follower: Int,\n        @SerialName(\"is_follow\")\n        val isFollow: Int,\n        val mid: Long,\n        @SerialName(\"nickname_color\")\n        val nicknameColor: String,\n        val pendant: Pendant,\n        @SerialName(\"theme_type\")\n        val themeType: Int,\n        val uname: String,\n        @SerialName(\"verify_type\")\n        val verifyType: Int,\n        @SerialName(\"vip_label\")\n        val vipLabel: Vip.Label,\n        @SerialName(\"vip_status\")\n        val vipStatus: Int,\n        @SerialName(\"vip_type\")\n        val vipType: Int\n    )\n\n    /**\n     * 用户信息\n     *\n     * @param areaLimit\n     * @param banAreaShow\n     * @param dialog 开通大会员提示文案，一般只在单独获取用户状态时且用户非会员时出现\n     * @param follow 是否追剧\n     * @param followStatus\n     * @param login 是否已登录\n     * @param pay\n     * @param payPackPaid\n     * @param progress 上次播放进度，仅登录时存在\n     * @param sponsor\n     * @param vipInfo 用户会员信息，仅登录时存在\n     */\n    @Serializable\n    data class UserStatus(\n        @SerialName(\"area_limit\")\n        val areaLimit: Int,\n        @SerialName(\"ban_area_show\")\n        val banAreaShow: Int,\n        val dialog: Dialog? = null,\n        val follow: Int,\n        @SerialName(\"follow_status\")\n        val followStatus: Int,\n        val login: Int,\n        val pay: Int,\n        @SerialName(\"pay_pack_paid\")\n        val payPackPaid: Int,\n        val progress: Progress? = null,\n        val sponsor: Int,\n        @SerialName(\"vip_info\")\n        val vipInfo: VipInfo? = null\n    ) {\n\n        /**\n         * 开通大会员按钮文案\n         */\n        @Serializable\n        data class Dialog(\n            @SerialName(\"btn_right\")\n            val btnRight: BtnRight,\n            val desc: String,\n            val title: String\n        ) {\n            @Serializable\n            data class BtnRight(\n                val title: String,\n                val type: String\n            )\n        }\n\n        /**\n         * 上次播放进度\n         *\n         * @param lastEpId 上次播放剧集epid\n         * @param lastEpIndex 上次播放剧集标题 [Episode.title]\n         * @param lastTime 上次播放时间\n         */\n        @Serializable\n        data class Progress(\n            @SerialName(\"last_ep_id\")\n            val lastEpId: Int,\n            @SerialName(\"last_ep_index\")\n            val lastEpIndex: String,\n            @SerialName(\"last_time\")\n            val lastTime: Int\n        )\n\n        /**\n         * 用户会员信息\n         *\n         * @param dueDate 会员到期时间\n         * @param status\n         * @param type\n         */\n        @Serializable\n        data class VipInfo(\n            @SerialName(\"due_date\")\n            val dueDate: Long,\n            val status: Int,\n            val type: Int\n        )\n    }\n}\n\n/**\n * 同系列其它季\n *\n * @param badge 标签，例如“独家”\n * @param badgeInfo\n * @param badgeType\n * @param cover 剧集封面图片url\n * @param horizontalCover 横向封面图片url，仅 App\n * @param link 剧集链接，仅 App\n * @param mediaId 剧集mdid，仅 Web\n * @param newEp  更新信息\n * @param report 仅 App\n * @param resource 仅 App\n * @param seasonId\n * @param seasonTitle 剧集短标题，用于 Tab 处显示\n * @param seasonType\n * @param stat 仅 Web\n * @param title 剧集完整长标题，仅 App\n */\n@Serializable\ndata class OtherSeason(\n    val badge: String = \"\",\n    @SerialName(\"badge_info\")\n    val badgeInfo: Episode.BadgeInfo? = null,\n    @SerialName(\"badge_type\")\n    val badgeType: Int,\n    val cover: String = \"\",\n    @SerialName(\"horizontal_cover\")\n    val horizontalCover: String? = null,\n    val link: String? = null,\n    @SerialName(\"media_id\")\n    val mediaId: Int? = null,\n    @SerialName(\"new_ep\")\n    val newEp: NewEP,\n    val report: Report? = null,\n    val resource: String? = null,\n    @SerialName(\"season_id\")\n    val seasonId: Int,\n    @SerialName(\"season_title\")\n    val seasonTitle: String,\n    @SerialName(\"season_type\")\n    val seasonType: Int? = null,\n    val stat: Stat? = null,\n    val title: String? = null\n) {\n    /**\n     * 剧集数据信息\n     *\n     * @param favorites 单季追剧人数\n     * @param seriesFollow 系列追剧人数\n     * @param views 播放次数\n     */\n    @Serializable\n    data class Stat(\n        val favorites: Int,\n        @SerialName(\"series_follow\")\n        val seriesFollow: Int,\n        val views: Long\n    )\n\n    @Serializable\n    data class Report(\n        @SerialName(\"display_type\")\n        val displayType: String,\n        @SerialName(\"season_id\")\n        val seasonId: String,\n        @SerialName(\"season_type\")\n        val seasonType: String,\n        @SerialName(\"version_style\")\n        val versionStyle: String\n    )\n}\n\n/**\n * 更新信息\n *\n * @param cover 封面\n * @param desc 更新备注\n * @param id 最新一话epid\n * @param indexShow 集数\n * @param _isNew 是否最新发布 0：否 1：是\n * @param title 最新一话标题\n */\n@Serializable\ndata class NewEP(\n    val cover: String = \"\",\n    val desc: String = \"\",\n    val id: Int,\n    @SerialName(\"index_show\")\n    val indexShow: String = \"\",\n    @SerialName(\"is_new\")\n    private val _isNew: Int = 0,\n    val title: String = \"\"\n) {\n    val isNew = _isNew == 1\n}\n\n/**\n * 发布信息\n *\n * @param _isFinish 完结状态 0：未完结 1：已完结\n * @param _isStarted 是否发布 0：未发布 1：已发布\n * @param pubTime 发布时间 YYYY-MM-DDD hh:mm:ss\n * @param pubTimeShow 发布时间文字介绍\n * @param releaseDateShow 发布日期文字介绍，仅 App 端，例如 \"2020年8月15日开播\"\n * @param timeLengthShow 应该可能是更新时间显示，仅 App 端，例如 \"已完结，全1话\"\n * @param unknowPubDate 0 作用尚不明确\n * @param updateInfoDesc 更新信息，仅 App 端，例如 \"更已完结，全1话\"\n * @param weekday 0 作用尚不明确\n */\n@Serializable\ndata class Publish(\n    @SerialName(\"is_finish\")\n    private val _isFinish: Int,\n    @SerialName(\"is_started\")\n    private val _isStarted: Int,\n    @SerialName(\"pub_time\")\n    val pubTime: String,\n    @SerialName(\"pub_time_show\")\n    val pubTimeShow: String,\n    @SerialName(\"release_date_show\")\n    val releaseDateShow: String? = null,\n    @SerialName(\"time_length_show\")\n    val timeLengthShow: String? = null,\n    @SerialName(\"unknow_pub_date\")\n    val unknowPubDate: Int,\n    @SerialName(\"update_info_desc\")\n    val updateInfoDesc: String? = null,\n    val weekday: Int\n) {\n    val isFinish = _isFinish == 1\n    val isStarted = _isStarted == 1\n}\n\n/**\n * 评分信息\n *\n * @param count 总计评分人数\n * @param score 评分\n */\n@Serializable\ndata class Rating(\n    val count: Int,\n    val score: Float\n)\n\n/**\n * 属性标志信息\n *\n * @param allowBp\n * @param allowBpRank\n * @param allowDownload\n * @param allowReview\n * @param areaLimit\n * @param banAreaShow\n * @param canWatch\n * @param copyright 版权标志 bilibili：授权 dujia：独家\n * @param copyrightName 版权名称，仅 App 端，例如 “独家”\n * @param forbidPre\n * @param isCoverShow\n * @param isPreview\n * @param onlyVipDownload\n * @param resource\n * @param watchPlatform\n */\n@Serializable\ndata class SeasonRights(\n    @SerialName(\"allow_bp\")\n    val allowBp: Int,\n    @SerialName(\"allow_bp_rank\")\n    val allowBpRank: Int,\n    @SerialName(\"allow_download\")\n    val allowDownload: Int,\n    @SerialName(\"allow_review\")\n    val allowReview: Int,\n    @SerialName(\"area_limit\")\n    val areaLimit: Int,\n    @SerialName(\"ban_area_show\")\n    val banAreaShow: Int,\n    @SerialName(\"can_watch\")\n    val canWatch: Int,\n    val copyright: String,\n    @SerialName(\"copyright_name\")\n    val copyrightName: String? = null,\n    @SerialName(\"forbid_pre\")\n    val forbidPre: Int,\n    @SerialName(\"is_cover_show\")\n    val isCoverShow: Int,\n    @SerialName(\"is_preview\")\n    val isPreview: Int,\n    @SerialName(\"only_vip_download\")\n    val onlyVipDownload: Int,\n    val resource: String,\n    @SerialName(\"watch_platform\")\n    val watchPlatform: Int\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/subtitle/Subtitle.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.subtitle\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n\n/**\n * 字幕信息\n * @param allowSubmit 是否允许提交字幕\n * @param list 字幕列表\n */\n@Serializable\ndata class Subtitle(\n    val allowSubmit: Boolean = false,\n    val list: List<SubtitleListItem> = emptyList()\n)\n\n/**\n * 字幕列表项\n *\n * @param id 字幕id\n * @param lan 字幕语言\n * @param lanDoc 字幕语言名称\n * @param isLock 是否锁定\n * @param authorMid 字幕上传者mid\n * @param subtitleUrl json格式字幕文件url\n * @param author 字幕上传者信息\n */\n@Serializable\ndata class SubtitleListItem(\n    val id: Long,\n    val lan: String,\n    @SerialName(\"lan_doc\")\n    val lanDoc: String,\n    @SerialName(\"is_lock\")\n    val isLock: Boolean,\n    @SerialName(\"author_mid\")\n    val authorMid: Long? = null,\n    @SerialName(\"subtitle_url\")\n    val subtitleUrl: String,\n    val author: SubtitleAuthor\n)\n\n/**\n * 字幕作者\n *\n * @param mid 字幕上传者mid\n * @param name 字幕上传者昵称\n * @param sex 字幕上传者性别 男 女 保密\n * @param face 字幕上传者头像url\n * @param sign 字幕上传者签名\n * @param rank 10000 作用尚不明确\n * @param birthday 0 作用尚不明确\n * @param isFakeAccount 0 作用尚不明确\n * @param isDeleted 0 作用尚不明确\n */\n@Serializable\ndata class SubtitleAuthor(\n    val mid: Long,\n    val name: String,\n    val sex: String,\n    val face: String,\n    val sign: String,\n    val rank: Int,\n    val birthday: Int,\n    @SerialName(\"is_fake_account\")\n    val isFakeAccount: Int,\n    @SerialName(\"is_deleted\")\n    val isDeleted: Int\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/toview/ToViewData.kt",
    "content": "// package dev.aaa1115910.biliapi.http.entity.history\npackage dev.aaa1115910.biliapi.http.entity.toview\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ToViewData(\n    // val cursor: Cursor,\n    // val tab: List<TabItem>,\n    val list: List<ToViewItem>\n) {\n    /**\n     * 历史记录页面信息\n     *\n     * @param max 最后一项目标 id 见请求参数\n     * @param viewAt 最后一项时间节点 时间戳\n     * @param business 最后一项业务类型 见请求参数\n     * @param ps 每页项数\n     */\n    @Serializable\n    data class Cursor(\n        val max: Long,\n        @SerialName(\"view_at\")\n        val viewAt: Long,\n        val business: String,\n        val ps: Int\n    )\n\n    /**\n     * 历史记录筛选类型\n     *\n     * @param type 类型\n     * @param name 类型名\n     */\n    @Serializable\n    data class TabItem(\n        val type: String,\n        val name: String\n    )\n}\n\n@Serializable\ndata class ToViewItem(\n    var aid: Long,\n    var bvid: String,\n    var cid: Long,\n    var owner: Owner,\n    val title: String,\n    // @SerialName(\"long_title\")\n    // val longTitle: String,\n    val pic: String,\n    // val cover: String,\n    // val covers: List<String>? = null,\n    // val uri: String,\n    // val history: HistoryInfo,\n    val videos: Int,\n    // @SerialName(\"author_name\")\n    // val authorName: String,\n    // @SerialName(\"author_face\")\n    // val authorFace: String,\n    // @SerialName(\"author_mid\")\n    // val authorMid: Long,\n    // @SerialName(\"view_at\")\n    // val viewAt: Int,\n    val progress: Int,\n    // val badge: String,\n    // @SerialName(\"show_title\")\n    // val showTitle: String,\n    val duration: Int,\n    // val current: String,\n    // val total: Int,\n    // @SerialName(\"new_desc\")\n    // val newDesc: String,\n    // @SerialName(\"is_finish\")\n    // val isFinish: Int,\n    // @SerialName(\"is_fav\")\n    // val isFav: Int,\n    // val kid: Int,\n    val pubdate: Long,\n    val stat: Stat\n) {\n    @Serializable\n    data class Owner(\n        val name: String,\n        val mid: Long,\n        val face: String\n    )\n    // @Serializable\n    // data class HistoryInfo(\n    //     val oid: Long,\n    //     val epid: Int,\n    //     val bvid: String,\n    //     val page: Int,\n    //     val cid: Long,\n    //     val part: String,\n    //     val business: String,\n    //     val dt: Int\n    // )\n\n    @Serializable\n    data class Stat(\n        val view: Long,\n        val danmaku: Int,\n        val reply: Int,\n        val favorite: Int,\n        val coin: Int,\n        val share: Int,\n        val like: Int\n    )\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/Follow.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 关注信息\n *\n * @param list 关注明细列表\n * @param reVersion\n * @param total 关注总数\n */\n@Serializable\ndata class UserFollowData(\n    val list: List<FollowedUser> = emptyList(),\n    @SerialName(\"re_version\")\n    val reVersion: Int,\n    val total: Int\n) {\n    /**\n     * 关注的用户信息\n     *\n     * @param mid 用户mid\n     * @param attribute 关注属性 0：未关注 2：已关注 6：已互粉\n     * @param mtime 关注对方时间 时间戳 互关后刷新\n     * @param tag 分组id 默认分组：null 存在至少一个分组：array\n     * @param special 特别关注标志 0：否 1：是\n     * @param uname 用户昵称\n     * @param face 用户头像url\n     * @param sign 用户签名\n     * @param officialVerify 认证信息\n     * @param vip 会员信息\n     * @param nftIcon\n     * @param recReason\n     * @param trackId\n     */\n    @Serializable\n    data class FollowedUser(\n        val mid: Long,\n        val attribute: Int,\n        val mtime: Int,\n        val tag: List<Int>? = null,\n        val special: Int,\n        val uname: String,\n        val face: String,\n        val sign: String,\n        @SerialName(\"official_verify\")\n        val officialVerify: OfficialVerify,\n        val vip: Vip,\n        @SerialName(\"nft_icon\")\n        val nftIcon: String,\n        @SerialName(\"rec_reason\")\n        val recReason: String,\n        @SerialName(\"track_id\")\n        val trackId: String\n    ) {\n        /**\n         * 会员信息\n         *\n         * @param vipType 会员类型 0：无 1：月度大会员 2：年度以上大会员\n         * @param vipDueDate 会员到期时间 时间戳 毫秒\n         * @param dueRemark\n         * @param accessStatus\n         * @param vipStatus 大会员状态 0：无 1：有\n         * @param vipStatusWarn\n         * @param themeType\n         * @param label\n         */\n        @Serializable\n        data class Vip(\n            val vipType: Int,\n            val vipDueDate: Long,\n            val dueRemark: String,\n            val accessStatus: Int,\n            val vipStatus: Int,\n            val vipStatusWarn: String,\n            val themeType: Int,\n            val label: dev.aaa1115910.biliapi.http.entity.user.Vip.Label\n        )\n    }\n}\n\nenum class FollowAction(val id: Int) {\n    AddFollow(1), DelFollow(2),\n    AddFollowQuietly(3), DelFollowQuietly(4),\n    AddBlackList(5), DelBlackList(6),\n    DelFan(7)\n}\n\nenum class FollowActionSource(val id: Int) {\n    Space(11), Video(14), Article(115), Activity(222)\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/LevelInfo.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 等级信息\n *\n * @param currentLevel 当前等级 0-6级\n * @param currentMin 当前等级经验最低值\n * @param currentExp 当前经验\n * @param nextExp 升级下一等级需达到的经验\n */\n@Serializable\ndata class LevelInfo(\n    @SerialName(\"current_level\")\n    val currentLevel: Int,\n    @SerialName(\"current_min\")\n    val currentMin: Int,\n    @SerialName(\"current_exp\")\n    val currentExp: Int,\n    @SerialName(\"next_exp\")\n    val nextExp: Int\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/Nameplate.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 勋章\n *\n * @param nid 勋章id\n * @param name 勋章名称\n * @param image 勋章图标\n * @param imageSmall 勋章图标（小）\n * @param level 勋章等级\n * @param condition 获取条件\n */\n@Serializable\ndata class Nameplate(\n    val nid: Int,\n    val name: String,\n    val image: String,\n    @SerialName(\"image_small\")\n    val imageSmall: String,\n    val level: String,\n    val condition: String\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/Official.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.Serializable\n\n/**\n * 认证信息\n *\n * @param role 认证类型 0：无 1 2 7 9：个人认证 3 4 5 6：机构认证\n * @param title 认证信息 无为空\n * @param desc 认证备注 无为空\n * @param type 是否认证 -1：无 0：个人认证 1：机构认证\n */\n@Serializable\ndata class Official(\n    val role: Int,\n    val title: String,\n    val desc: String,\n    val type: Int\n)\n\n/**\n * 认证信息\n *\n * @param type 是否认证 -1：无 0：认证\n * @param desc 认证信息 无为空\n */\n@Serializable\ndata class OfficialVerify(\n    val type: Int,\n    val desc: String\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/Pendant.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 头像框\n *\n * @param pid 头像框id\n * @param name 头像框名称\n * @param image 头像框图片url\n * @param expire 过期时间 此接口返回恒为0\n * @param imageEnhance 头像框图片url\n * @param imageEnhanceFrame 头像框图片逐帧序列url\n */\n@Serializable\ndata class Pendant(\n    val pid: Int,\n    val name: String,\n    val image: String,\n    val expire: Int = 0,\n    @SerialName(\"image_enhance\")\n    val imageEnhance: String? = null,\n    @SerialName(\"image_enhance_frame\")\n    val imageEnhanceFrame: String? = null\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/Profession.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 专业资质信息\n *\n * @param name 资质名称\n * @param department 职位\n * @param title 所属机构\n * @param isShow 是否显示 0：不显示 1：显示\n */\n@Serializable\ndata class Profession(\n    val name: String,\n    val department: String,\n    val title: String,\n    @SerialName(\"is_show\")\n    val isShow: Int\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/Relation.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport dev.aaa1115910.biliapi.http.util.CommonEnumIntSerializer\nimport dev.aaa1115910.biliapi.http.util.SerialEnum\nimport dev.aaa1115910.biliapi.http.util.serial\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\n\n/**\n * 相互关系\n *\n * @param relation 目标用户对于本用户的属性\n * @param beRelation 本用户对于目标用户的属性\n */\n@Serializable\ndata class RelationData(\n    val relation: Relation,\n    @SerialName(\"be_relation\")\n    val beRelation: Relation\n)\n\n/**\n * 用户关系\n *\n * @param mid 用户mid\n * @param attribute 关注属性\n * @param mtime 关注对方时间 互关后刷新时间\n * @param tag 分组id null默认分组 array存在至少一个分组\n * @param special 特别关注标志 0：否 1：是\n * @param isSpecialFollowing 是否已特别关注\n */\n@Serializable\ndata class Relation(\n    val mid: Long,\n    val attribute: RelationType,\n    val mtime: Int,\n    val tag: List<Int>? = null,\n    private val special: Int,\n    @Transient\n    val isSpecialFollowing: Boolean = special == 1\n)\n\nprivate object RelationTypeSerializer : CommonEnumIntSerializer<RelationType>(\n    \"RelationType\",\n    RelationType.entries.toTypedArray(),\n    RelationType.entries.toTypedArray().serial()\n)\n\n@Serializable(with = RelationTypeSerializer::class)\nenum class RelationType(override val serialNumber: Int) : SerialEnum {\n    None(0), FollowedQuietly(1), Followed(2), BothFollowed(6), BlackList(128)\n}\n\n@Serializable\ndata class RelationStat(\n    val black: Int,\n    val follower: Int,\n    val following: Int,\n    val mid: Long,\n    val whisper: Int\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/SpaceVideoData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport dev.aaa1115910.biliapi.http.entity.video.VideoStat\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\nimport kotlinx.serialization.json.JsonElement\n\n/**\n * 空间投稿视频\n *\n * @param list 列表信息\n * @param page 页面信息\n * @param episodicButton “播放全部“按钮\n * @param isRisk\n * @param gaiaResType\n * @param gaiaData\n * @param vVoucher 风控\n */\n@Serializable\ndata class WebSpaceVideoData(\n    val list: SpaceVideoListItem? = null,\n    val page: Page? = null,\n    @SerialName(\"episodic_button\")\n    val episodicButton: EpisodicButton? = null,\n    @SerialName(\"is_risk\")\n    val isRisk: Boolean? = null,\n    @SerialName(\"gaia_res_type\")\n    val gaiaResType: Int? = null,\n    @SerialName(\"gaia_data\")\n    val gaiaData: JsonElement? = null,\n    @SerialName(\"v_voucher\")\n    val vVoucher: String? = null,\n) {\n    /**\n     * @param tlist 投稿视频分区索引\n     * @param vlist 投稿视频列表\n     */\n    @Serializable\n    data class SpaceVideoListItem(\n        val tlist: Map<String, Tid>? = null,\n        val vlist: List<VListItem>\n    ) {\n        /**\n         * @param count 投稿至该分区的视频数\n         * @param name 该分区名称\n         * @param tid 该分区tid\n         */\n        @Serializable\n        data class Tid(\n            var tid: Int,\n            val count: Int,\n            val name: String\n        )\n\n        /**\n         * @param aid 稿件avid\n         * @param author 视频UP主 不一定为目标用户（合作视频）\n         * @param bvid 稿件bvid\n         * @param comment 视频评论数\n         * @param copyright\n         * @param created 投稿时间 时间戳\n         * @param description 视频简介\n         * @param hideClick\n         * @param isPay\n         * @param isUnionVideo 是否为合作视频 0：否 1：是\n         * @param length 视频长度 MM:SS\n         * @param mid 视频UP主mid 不一定为目标用户（合作视频）\n         * @param pic 视频封面\n         * @param play 视频播放次数 如果视频基本信息API对应的状态为-403视频访问权限不足，数据类型将变为str，如（\"play\": \"--\",），于mid79发表的av5132474可见\n         * @param review 作用尚不明确\n         * @param subtitle\n         * @param title 视频标题\n         * @param typeid 视频分区tid\n         * @param videoReview 视频弹幕数\n         * @param isSteinsGate\n         * @param isLivePlayback\n         * @param meta 合集信息\n         * @param isAvoided\n         * @param attribute\n         */\n        @Serializable\n        data class VListItem(\n            val aid: Long,\n            val bvid: String,\n            val author: String,\n            val comment: Int,\n            val copyright: String,\n            val created: Long,\n            val description: String,\n            @SerialName(\"hide_click\")\n            val hideClick: Boolean,\n            @SerialName(\"is_pay\")\n            val isPay: Int,\n            @SerialName(\"is_union_video\")\n            val isUnionVideo: Int,\n            val length: String,\n            val mid: Long,\n            val pic: String,\n            val play: Long,\n            val review: Int,\n            val subtitle: String,\n            val title: String,\n            val typeid: Int,\n            @SerialName(\"video_review\")\n            val videoReview: Int,\n            @SerialName(\"is_steins_gate\")\n            val isSteinsGate: Int,\n            @SerialName(\"is_live_playback\")\n            val isLivePlayback: Int,\n            val meta: Meta? = null,\n            @SerialName(\"is_avoided\")\n            private val _isAvoided: Int,\n            @Transient\n            val isAvoided: Boolean = _isAvoided == 1,\n            @SerialName(\"attribute\")\n            val attribute: Int,\n            @SerialName(\"is_charging_arc\")\n            val isChargingArc: Boolean = false,\n            @SerialName(\"elec_arc_badge\")\n            val elecArcBadge: String = \"\"\n        ) {\n            /**\n             * 合集信息\n             */\n            @Serializable\n            data class Meta(\n                val id: Int,\n                val title: String,\n                val cover: String,\n                val mid: Long,\n                val intro: String,\n                @SerialName(\"sign_state\")\n                val signState: Int,\n                val attribute: Int,\n                val stat: VideoStat,\n                @SerialName(\"ep_count\")\n                val epCount: Int,\n                @SerialName(\"first_aid\")\n                val firstAid: Long? = null,\n                val ptime: Int,\n                @SerialName(\"ep_num\")\n                val epNum: Int\n            )\n        }\n    }\n\n    /**\n     * @param pageNumber 当前页码\n     * @param pageSize 每页项数\n     * @param count 总计稿件数\n     */\n    @Serializable\n    data class Page(\n        @SerialName(\"pn\")\n        val pageNumber: Int,\n        @SerialName(\"ps\")\n        val pageSize: Int,\n        val count: Int,\n    )\n\n}\n\n/**\n * 空间投稿视频（App）\n *\n * @param episodicButton “播放全部“按钮\n * @param order 排序方式\n * @param count 视频总数\n */\n@Serializable\ndata class AppSpaceVideoData(\n    @SerialName(\"episodic_button\")\n    val episodicButton: EpisodicButton? = null,\n    val order: List<Order> = emptyList(),\n    val count: Int,\n    val item: List<SpaceVideoItem> = emptyList(),\n    @SerialName(\"last_watched_locator\")\n    val lastWatchedLocator: LastWatchedLocator,\n    @SerialName(\"has_next\")\n    val hasNext: Boolean,\n    val hasPre: Boolean = false\n) {\n    /**\n     * 排序方式\n     *\n     * @param title 排序方式名称 最新发布/最多播放\n     * @param value 排序方式值 pubdate/click\n     */\n    @Serializable\n    data class Order(\n        val title: String,\n        val value: String\n    )\n\n    @Serializable\n    data class SpaceVideoItem(\n        val title: String,\n        val subtitle: String,\n        val tname: String,\n        val cover: String,\n        val uri: String,\n        val param: String,\n        val goto: String,\n        val length: String,\n        val duration: Int,\n        @SerialName(\"is_popular\")\n        val isPopular: Boolean,\n        @SerialName(\"is_steins\")\n        val isSteins: Boolean,\n        @SerialName(\"is_ugcpay\")\n        val isUgcpay: Boolean,\n        @SerialName(\"is_cooperation\")\n        val isCooperation: Boolean,\n        @SerialName(\"is_pgc\")\n        val isPgc: Boolean,\n        @SerialName(\"is_live_playback\")\n        val isLivePlayback: Boolean,\n        val play: Long,\n        val danmaku: Int,\n        val ctime: Int,\n        @SerialName(\"ugc_pay\")\n        val ugcPay: Int,\n        val author: String? = null,\n        val state: Boolean,\n        val bvid: String? = null,\n        val videos: Int,\n        @SerialName(\"three_point\")\n        val threePoint: List<ThreePointItem> = emptyList(),\n        @SerialName(\"first_cid\")\n        val firstcid: Long? = null,\n        @SerialName(\"cursor_attr\")\n        val cursorAttr: CursorAttr,\n        @SerialName(\"icon_type\")\n        val iconType: Int\n    ) {\n        @Serializable\n        data class ThreePointItem(\n            val type: String,\n            val icon: String,\n            val text: String,\n            @SerialName(\"share_succ_toast\")\n            val shareSuccToast: String? = null,\n            @SerialName(\"share_fail_toast\")\n            val shareFailToast: String? = null,\n            @SerialName(\"share_path\")\n            val sharePath: String? = null,\n            @SerialName(\"short_link\")\n            val shortLink: String? = null,\n            @SerialName(\"share_subtitle\")\n            val shareSubtitle: String? = null\n        )\n\n        @Serializable\n        data class CursorAttr(\n            @SerialName(\"is_last_watched_arc\")\n            val isLastWatchedArc: Boolean,\n            val rank: Int\n        )\n    }\n\n    @Serializable\n    data class LastWatchedLocator(\n        @SerialName(\"display_threshold\")\n        val displayThreshold: Int,\n        @SerialName(\"insert_ranking\")\n        val insertRanking: Int,\n        val text: String\n    )\n}\n\n/**\n * 全部播放按钮\n *\n * @param text 按钮文字\n * @param uri 全部播放页url\n */\n@Serializable\ndata class EpisodicButton(\n    val text: String,\n    val uri: String\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/Staff.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.Serializable\n\n/**\n * 创作者个人信息\n *\n * @param mid 成员mid\n * @param title 成员名称\n * @param name 成员昵称\n * @param face 成员头像url\n * @param vip 成员大会员状态\n * @param official 成员认证信息\n * @param follower 成员粉丝数\n */\n@Serializable\ndata class Staff(\n    val mid: Long,\n    val title: String,\n    val name: String,\n    val face: String,\n    val vip: Vip,\n    val official: Official,\n    val follower: Int\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/UserCardInfoResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 用户卡片信息\n *\n * @param card 详细信息\n * @param space 主页头图\n * @param following 是否关注此用户 true：已关注 false：未关注 需要登录(Cookie) 未登录为false\n * @param archiveCount 用户稿件数\n * @param articleCount 0 作用尚不明确\n * @param follower 粉丝数\n * @param likeNum 获赞数\n */\n@Serializable\ndata class UserCardData(\n    val card: UserCardInfo,\n    val space: Space? = null,\n    val following: Boolean,\n    @SerialName(\"archive_count\")\n    val archiveCount: Int,\n    @SerialName(\"article_count\")\n    val articleCount: Int,\n    val follower: Int,\n    @SerialName(\"like_num\")\n    val likeNum: Int\n) {\n    /**\n     * 用户卡片详细信息\n     *\n     * @param mid 用户mid\n     * @param name 昵称\n     * @param approve\n     * @param sex 性别 男/女/保密\n     * @param rank 用户权限等级 目前应该无任何作用 5000：0级未答题 10000：普通会员 20000：字幕君 25000：VIP 30000：真·职人 32000：管理员\n     * @param face 头像链接\n     * @param faceNft 是否为 nft 头像 0不是nft头像 1是 nft 头像\n     * @param faceNftType 0,1\n     * @param displayRank\n     * @param regtime\n     * @param spacesta\n     * @param birthday\n     * @param place\n     * @param description\n     * @param article\n     * @param attentions\n     * @param fans 粉丝数\n     * @param friend 粉丝数\n     * @param attention 粉丝数\n     * @param sign 签名\n     * @param levelInfo 等级\n     * @param pendant 头像框信息\n     * @param nameplate 勋章信息\n     * @param official 认证信息\n     * @param officialVerify 认证信息\n     * @param vip 大会员信息\n     * @param isSeniorMember 是否为硬核会员 0：否 1：是\n     */\n    @Serializable\n    data class UserCardInfo(\n        val mid: String,\n        val name: String,\n        val approve: Boolean,\n        val sex: String,\n        val rank: Int,\n        val face: String,\n        @SerialName(\"face_nft\")\n        val faceNft: Int,\n        @SerialName(\"face_nft_type\")\n        val faceNftType: Int,\n        @SerialName(\"DisplayRank\")\n        val displayRank: String,\n        val regtime: Int,\n        val spacesta: Int,\n        val birthday: String,\n        val place: String,\n        val description: String,\n        val article: Int,\n        //val attentions: List<Any> = emptyList(),\n        val fans: Int,\n        val friend: Int,\n        val attention: Int,\n        val sign: String,\n        @SerialName(\"level_info\")\n        val levelInfo: LevelInfo,\n        val pendant: Pendant,\n        val nameplate: Nameplate,\n        @SerialName(\"Official\")\n        val official: Official,\n        @SerialName(\"official_verify\")\n        val officialVerify: OfficialVerify,\n        val vip: Vip,\n        @SerialName(\"is_senior_member\")\n        val isSeniorMember: Int\n    )\n\n    /**\n     * 主页头图\n     *\n     * @param sImg 主页头图url 小图\n     * @param lImg 主页头图url 正常\n     */\n    @Serializable\n    data class Space(\n        @SerialName(\"s_img\")\n        val sImg: String,\n        @SerialName(\"l_img\")\n        val lImg: String\n    )\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/UserGarb.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 用户头像挂件\n *\n * @param urlImageAniCut\n */\n@Serializable\ndata class UserGarb(\n    @SerialName(\"url_image_ani_cut\")\n    val urlImageAniCut: String\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/UserHonours.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class UserHonours(\n    val mid: Long,\n    val colour: Colour? = null,\n    val tags: List<String> = emptyList()\n) {\n    @Serializable\n    data class Colour(\n        val dark: String,\n        val normal: String\n    )\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/UserInfoResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 用户信息\n *\n * @param mid 用户mid\n * @param name 昵称\n * @param sex 性别 男/女/保密\n * @param face 头像链接\n * @param faceNft 是否为 nft 头像 0不是nft头像 1是 nft 头像\n * @param faceNftType 0,1\n * @param sign 签名\n * @param rank 用户权限等级 目前应该无任何作用 5000：0级未答题 10000：普通会员 20000：字幕君 25000：VIP 30000：真·职人 32000：管理员\n * @param level 当前等级 0-6级\n * @param jointime 注册时间 此接口返回恒为0\n * @param moral 节操值 此接口返回恒为0\n * @param silence 封禁状态 0：正常 1：被封\n * @param coins 硬币数 需要登录(Cookie) 只能查看自己的 默认为0\n * @param fansBadge 是否具有粉丝勋章 false：无 true：有\n * @param fansMedal 粉丝勋章信息\n * @param official 认证信息\n * @param vip 会员信息\n * @param pendant 头像框信息\n * @param nameplate 勋章信息\n * @param userHonourInfo\n * @param isFollowed 是否关注此用户 true：已关注 false：未关注 需要登录(Cookie) 未登录恒为false\n * @param topPhoto 主页头图链接\n * @param theme 空 作用尚不明确\n * @param sys_notice 系统通知 无内容则为空对象 主要用于展示如用户争议、纪念账号等等\n * @param live_room 直播间信息\n * @param birthday 生日 MM-DD 如设置隐私为空\n * @param school 学校\n * @param profession 专业资质信息\n * @param tags 个人标签\n * @param series\n * @param isSeniorMember 是否为硬核会员 0：否 1：是\n * @param mcn_info\n * @param gaiaResType\n * @param gaia_data\n * @param isRisk\n * @param elec 充电信息\n * @param contract 是否显示老粉计划\n */\n@Serializable\ndata class UserInfoData(\n    val mid: Long,\n    val name: String,\n    val sex: String,\n    val face: String,\n    @SerialName(\"face_nft\")\n    val faceNft: Int,\n    @SerialName(\"face_nft_type\")\n    val faceNftType: Int,\n    val sign: String,\n    val rank: Int,\n    val level: Int,\n    val jointime: Int,\n    val moral: Int,\n    val silence: Int,\n    val coins: Float,\n    @SerialName(\"fans_badge\")\n    val fansBadge: Boolean,\n//    @SerialName(\"fans_medal\")\n//    val fansMedal: FansMedal,\n    val official: Official,\n    val vip: Vip,\n    val pendant: Pendant,\n    val nameplate: Nameplate,\n    @SerialName(\"user_honour_info\")\n    val userHonourInfo: UserHonours,\n    @SerialName(\"is_followed\")\n    val isFollowed: Boolean,\n    @SerialName(\"top_photo\")\n    val topPhoto: String,\n    //val theme: Any? = null,\n    val sys_notice: SysNotice,\n    val live_room: LiveRoom? = null,\n    val birthday: String,\n    val school: School? = null,\n    val profession: Profession,\n    //val tags: Any? = null,\n    val series: Series,\n    @SerialName(\"is_senior_member\")\n    val isSeniorMember: Int,\n    //val mcn_info: Any? = null,\n    @SerialName(\"gaia_res_type\")\n    val gaiaResType: Int,\n    //val gaia_data: Any? = null,\n    @SerialName(\"is_risk\")\n    val isRisk: Boolean,\n    val elec: Elec,\n    val contract: Contract? = null\n) {\n    @Serializable\n    data class FansMedal(\n        val show: Boolean,\n        val wear: Boolean,\n        val medal: Medal? = null\n    ) {\n        /**\n         * 粉丝勋章\n         *\n         * @param uid 此用户mid\n         * @param targetId 粉丝勋章所属UP的mid\n         * @param medalId 粉丝勋章id\n         * @param level 粉丝勋章等级\n         * @param medalName 粉丝勋章名称\n         * @param medalColor 颜色\n         * @param intimacy 当前亲密度\n         * @param nextIntimacy 下一等级所需亲密度\n         * @param dayLimit 每日亲密度获取上限\n         * @param todayFeed 今日已获得亲密度\n         * @param medalColorStart 粉丝勋章颜色 十进制数，可转为十六进制颜色代码\n         * @param medalColorEnd 粉丝勋章颜色 十进制数，可转为十六进制颜色代码\n         * @param medalColorBorder 粉丝勋章边框颜色 十进制数，可转为十六进制颜色代码\n         * @param isLighted\n         * @param lightStatus\n         * @param wearingStatus 当前是否佩戴 0：未佩戴 1：已佩戴\n         * @param score\n         */\n        @Serializable\n        data class Medal(\n            val uid: Long,\n            @SerialName(\"target_id\")\n            val targetId: Long,\n            @SerialName(\"medal_id\")\n            val medalId: Int,\n            val level: Long,\n            @SerialName(\"medal_name\")\n            val medalName: String,\n            @SerialName(\"medal_color\")\n            val medalColor: Int,\n            val intimacy: Int,\n            @SerialName(\"next_intimacy\")\n            val nextIntimacy: Int,\n            @SerialName(\"day_limit\")\n            val dayLimit: Int,\n            @SerialName(\"today_feed\")\n            val todayFeed: Int,\n            @SerialName(\"medal_color_start\")\n            val medalColorStart: Int,\n            @SerialName(\"medal_color_end\")\n            val medalColorEnd: Int,\n            @SerialName(\"medal_color_border\")\n            val medalColorBorder: Int,\n            @SerialName(\"is_lighted\")\n            val isLighted: Int,\n            @SerialName(\"light_status\")\n            val lightStatus: Int,\n            @SerialName(\"wearing_status\")\n            val wearingStatus: Int,\n            val score: Int\n        )\n    }\n\n\n    /**\n     * 系统提示\n     * 如用户争议、纪念账号等等\n     *\n     * @param id id\n     * @param content 显示文案\n     * @param url 跳转地址\n     * @param noticeType 提示类型 1,2\n     * @param icon 前缀图标\n     * @param textColor 文字颜色\n     * @param bgColor 背景颜色\n     */\n    @Serializable\n    data class SysNotice(\n        val id: Int? = null,\n        val content: String? = null,\n        val url: String? = null,\n        @SerialName(\"notice_type\")\n        val noticeType: Int? = null,\n        val icon: String? = null,\n        @SerialName(\"text_color\")\n        val textColor: String? = null,\n        @SerialName(\"bg_color\")\n        val bgColor: String? = null\n    )\n\n    /**\n     *\n     * @param roomStatus 直播间状态 0：无房间 1：有房间\n     * @param liveStatus 直播状态 0：未开播 1：直播中\n     * @param url 直播间网页 url\n     * @param title 直播间标题\n     * @param cover 直播间封面 url\n     * @param watchedShow\n     * @param roomId 直播间 id(短号)\n     * @param roundStatus 轮播状态 0：未轮播 1：轮播\n     * @param broadcastType 0\n     */\n    @Serializable\n    data class LiveRoom(\n        val roomStatus: Int,\n        val liveStatus: Int,\n        val url: String,\n        val title: String,\n        val cover: String,\n        @SerialName(\"watched_show\")\n        val watchedShow: WatchedShow,\n        @SerialName(\"roomid\")\n        val roomId: Int,\n        val roundStatus: Int,\n        @SerialName(\"broadcast_type\")\n        val broadcastType: Int\n    ) {\n        @Serializable\n        data class WatchedShow(\n            val switch: Boolean,\n            val num: Int,\n            @SerialName(\"text_small\")\n            val textSmall: String,\n            @SerialName(\"text_large\")\n            val textLarge: String,\n            val icon: String,\n            @SerialName(\"icon_location\")\n            val iconLocation: String,\n            @SerialName(\"icon_web\")\n            val iconWeb: String\n        )\n    }\n\n    /**\n     * 学校\n     *\n     * @param name 就读大学名称\n     */\n    @Serializable\n    data class School(\n        val name: String\n    )\n\n    @Serializable\n    data class Series(\n        @SerialName(\"user_upgrade_status\")\n        val userUpgradeStatus: Int,\n        @SerialName(\"show_upgrade_window\")\n        val showUpgradeWindow: Boolean\n    )\n\n    /**\n     * 充电\n     *\n     * @param showInfo 显示信息\n     */\n    @Serializable\n    data class Elec(\n        @SerialName(\"show_info\")\n        val showInfo: ElecShowInfo\n    ) {\n        /**\n         * 充电显示信息\n         *\n         * @param show 是否开通了充电\n         * @param state 状态 -1：未开通 1：已开通\n         * @param title 空串\n         * @param icon 空串\n         * @param jumpUrl 空串\n         */\n        @Serializable\n        data class ElecShowInfo(\n            val show: Boolean,\n            val state: Int,\n            val title: String,\n            val icon: String,\n            @SerialName(\"jump_url\")\n            val jumpUrl: String\n        )\n    }\n\n    /**\n     * 老粉计划\n     *\n     * @param isDisplay true/false 在页面中未使用此字段\n     * @param isFollowDisplay 是否在显示老粉计划 true：显示 false：不显示\n     */\n    @Serializable\n    data class Contract(\n        @SerialName(\"is_display\")\n        val isDisplay: Boolean,\n        @SerialName(\"is_follow_display\")\n        val isFollowDisplay: Boolean\n    )\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/UserSelfInfoResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 我的信息\n *\n * @param mid 用户mid\n * @param name 昵称\n * @param sex 性别 男/女/保密\n * @param face 头像链接\n * @param sign 签名\n * @param rank 用户权限等级 目前应该无任何作用 5000：0级未答题 10000：普通会员 20000：字幕君 25000：VIP 30000：真·职人 32000：管理员\n * @param level 当前等级 0-6级\n * @param jointime 注册时间 此接口返回恒为0\n * @param moral 节操 默认70\n * @param silence 封禁状态 0：正常 1：被封\n * @param emailStatus 已验证邮箱 0：未验证 1：已验证\n * @param telStatus 已验证手机号\t 0：未验证 1：已验证\n * @param identification 1\n * @param vip 会员信息\n * @param pendant 头像框信息\n * @param nameplate 勋章信息\n * @param official 认证信息\n * @param birthday 生日 时间戳\n * @param isTourist 会员信息\n * @param isFakeAccount\n * @param pinPrompting\n * @param isDeleted\n * @param inRegAudit\n * @param isRipUser\n * @param profession 专业资质信息\n * @param faceNft 是否为 nft 头像 0不是nft头像 1是 nft 头像\n * @param faceNftNew\n * @param isSeniorMember 是否为硬核会员 0：否 1：是\n * @param honours\n * @param digitalId\n * @param digitalType\n * @param levelExp\n * @param coins 硬币数\n * @param following 粉丝数\n * @param follower 粉丝数\n */\n@Serializable\ndata class MyInfoData(\n    val mid: Long,\n    val name: String,\n    val sex: String,\n    val face: String,\n    val sign: String,\n    val rank: Int,\n    val level: Int,\n    val jointime: Int,\n    val moral: Int,\n    val silence: Int,\n    @SerialName(\"email_status\")\n    val emailStatus: Int,\n    @SerialName(\"tel_status\")\n    val telStatus: Int,\n    val identification: Int,\n    val vip: Vip,\n    val pendant: Pendant,\n    val nameplate: Nameplate,\n    val official: Official,\n    val birthday: Long,\n    @SerialName(\"is_tourist\")\n    val isTourist: Int,\n    @SerialName(\"is_fake_account\")\n    val isFakeAccount: Int,\n    @SerialName(\"pin_prompting\")\n    val pinPrompting: Int,\n    @SerialName(\"is_deleted\")\n    val isDeleted: Int,\n    @SerialName(\"in_reg_audit\")\n    val inRegAudit: Int,\n    @SerialName(\"is_rip_user\")\n    val isRipUser: Boolean,\n    val profession: Profession,\n    @SerialName(\"face_nft\")\n    val faceNft: Int,\n    @SerialName(\"face_nft_new\")\n    val faceNftNew: Int,\n    @SerialName(\"is_senior_member\")\n    val isSeniorMember: Int,\n    val honours: UserHonours,\n    @SerialName(\"digital_id\")\n    val digitalId: String,\n    @SerialName(\"digital_type\")\n    val digitalType: Int,\n    @SerialName(\"level_exp\")\n    val levelExp: LevelInfo,\n    val coins: Float,\n    val following: Int,\n    val follower: Int\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/Vip.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 会员信息\n *\n * @param type 成员会员类型 0：无 1：月会员 2：年会员\n * @param status 会员状态 0：无 1：有\n * @param dueDate 会员过期时间 Unix时间戳(毫秒)\n * @param vipPayType 支付类型 0：未支付（常见于官方账号） 1：已支付（以正常渠道获取的大会员均为此值）\n * @param themeType 0\n * @param label 会员标签\n * @param avatarSubscript 是否显示会员图标 0：不显示 1：显示\n * @param nicknameColor 会员昵称颜色 颜色码，一般为#FB7299，曾用于愚人节改变大会员配色\n * @param role 大角色类型 1：月度大会员 3：年度大会员 7：十年大会员 15：百年大会员\n * @param avatarSubscriptUrl 大会员角标地址\n * @param tvVipStatus 电视大会员状态 0：未开通\n * @param tvVipPayType 电视大会员支付类型\n */\n@Serializable\ndata class Vip(\n    val type: Int,\n    val status: Int,\n    @SerialName(\"due_date\")\n    val dueDate: Long,\n    @SerialName(\"vip_pay_type\")\n    val vipPayType: Int = 0,\n    @SerialName(\"theme_type\")\n    val themeType: Int,\n    val label: Label,\n    @SerialName(\"avatar_subscript\")\n    val avatarSubscript: Int,\n    @SerialName(\"nickname_color\")\n    val nicknameColor: String,\n    val role: Int = 0,\n    @SerialName(\"avatar_subscript_url\")\n    val avatarSubscriptUrl: String,\n    @SerialName(\"tv_vip_status\")\n    val tvVipStatus: Int = 0,\n    @SerialName(\"tv_vip_pay_type\")\n    val tvVipPayType: Int = 0\n) {\n    /**\n     * 大会员标签\n     *\n     * @param path 空 作用尚不明确\n     * @param text 会员类型文案 大会员 年度大会员 十年大会员 百年大会员 最强绿鲤鱼\n     * @param labelTheme 会员标签 vip：大会员 annual_vip：年度大会员 ten_annual_vip：十年大会员 hundred_annual_vip：百年大会员 fools_day_hundred_annual_vip：最强绿鲤鱼\n     * @param textColor 会员标签\n     * @param bgStyle 1\n     * @param bgColor 会员标签背景颜色 颜色码，一般为#FB7299，曾用于愚人节改变大会员配色\n     * @param borderColor 会员标签边框颜色 未使用\n     * @param useImgLabel true\n     * @param imgLabelUriHans 空串\n     * @param imgLabelUriHant 空串\n     * @param imgLabelUriHansStatic 大会员牌子图片 简体版\n     * @param imgLabelUriHantStatic 大会员牌子图片 繁体版\n     */\n    @Serializable\n    data class Label(\n        val path: String = \"\",\n        val text: String,\n        @SerialName(\"label_theme\")\n        val labelTheme: String = \"\",\n        @SerialName(\"text_color\")\n        val textColor: String,\n        @SerialName(\"bg_style\")\n        val bgStyle: Int,\n        @SerialName(\"bg_color\")\n        val bgColor: String,\n        @SerialName(\"border_color\")\n        val borderColor: String,\n        @SerialName(\"use_img_label\")\n        val useImgLabel: Boolean = false,\n        @SerialName(\"img_label_uri_hans\")\n        val imgLabelUriHans: String = \"\",\n        @SerialName(\"img_label_uri_hant\")\n        val imgLabelUriHant: String = \"\",\n        @SerialName(\"img_label_uri_hans_static\")\n        val imgLabelUriHansStatic: String = \"\",\n        @SerialName(\"img_label_uri_hant_static\")\n        val imgLabelUriHantStatic: String = \"\"\n    )\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/favorite/CntInfo.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user.favorite\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * @param collect 收藏数\n * @param play 播放数\n * @param thumbUp 点赞数\n * @param share 分享数\n */\n@Serializable\ndata class CntInfo(\n    val collect: Int,\n    val play: Long,\n    val danmaku: Int = 0,\n    @SerialName(\"thumb_up\")\n    val thumbUp: Int = 0,\n    val share: Int = 0\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/favorite/FavoriteFolderInfo.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user.favorite\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 收藏夹元数据\n *\n * @param id 收藏夹mlid（完整id） 收藏夹原始id+创建者mid尾号2位\n * @param fid 收藏夹原始id\n * @param mid 创建者mid\n * @param attr 属性位（？）\n * @param title 收藏夹标题\n * @param cover 收藏夹封面图片url\n * @param upper 创建者信息\n * @param coverType 封面图类别（？）\n * @param cntInfo 收藏夹状态数\n * @param type 类型（？） 一般是11\n * @param intro 备注\n * @param ctime 创建时间\t时间戳\n * @param mtime 收藏时间\t时间戳\n * @param state 状态（？） 一般为0\n * @param favState 收藏夹收藏状态 已收藏收藏夹：1 未收藏收藏夹：0 需要登录\n * @param likeState 点赞状态 已点赞：1 未点赞：0 需要登录\n * @param mediaCount 收藏夹内容数量\n */\n@Serializable\ndata class FavoriteFolderInfo(\n    val id: Long,\n    val fid: Long,\n    val mid: Long,\n    val attr: Int,\n    val title: String,\n    val cover: String,\n    val upper: Upper,\n    @SerialName(\"cover_type\")\n    val coverType: Int,\n    @SerialName(\"cnt_info\")\n    val cntInfo: CntInfo,\n    val type: Int,\n    val intro: String,\n    val ctime: Long,\n    val mtime: Long,\n    val state: Int,\n    @SerialName(\"fav_state\")\n    val favState: Int,\n    @SerialName(\"like_state\")\n    val likeState: Int,\n    @SerialName(\"media_count\")\n    val mediaCount: Int\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/favorite/FavoriteFolderInfoListData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user.favorite\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 收藏夹信息及内容\n *\n * @param info 收藏夹元数据\n * @param medias 收藏夹内容\n * @param hasMore 还有更多数据\n */\n@Serializable\ndata class FavoriteFolderInfoListData(\n    val info: FavoriteFolderInfo,\n    val medias: List<FavoriteItem> = emptyList(),\n    @SerialName(\"has_more\")\n    val hasMore: Boolean\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/favorite/FavoriteItem.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user.favorite\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 收藏夹内容\n *\n * @param id 内容id 视频稿件：视频稿件avid 音频：音频auid 视频合集：视频合集id\n * @param type 内容类型 2：视频稿件 12：音频 21：视频合集\n * @param title 标题\n * @param cover 封面url\n * @param intro 简介\n * @param page 视频分P数\n * @param duration 音频/视频时长\n * @param upper UP主信息\n * @param attr 属性位（？）\n * @param cntInfo 状态数\n * @param link 跳转uri\n * @param ctime 投稿时间\t时间戳\n * @param pubtime 发布时间 时间戳\n * @param favTime 收藏时间 时间戳\n * @param bvid 视频稿件bvid\n */\n@Serializable\ndata class FavoriteItem(\n    val id: Long,\n    val type: Int,\n    val title: String,\n    val cover: String,\n    val intro: String,\n    val page: Int,\n    val duration: Int,\n    val upper: Upper,\n    val attr: Int,\n    @SerialName(\"cnt_info\")\n    val cntInfo: CntInfo,\n    val link: String,\n    val ctime: Long,\n    val pubtime: Long,\n    @SerialName(\"fav_time\")\n    val favTime: Long,\n    val bvid: String,\n)\n\n@Serializable\ndata class FavoriteItemIdListResponse(\n    val code: Int,\n    val message: String,\n    val data: List<FavoriteItemId>? = null\n)\n\n/**\n * 收藏夹内容 ID\n *\n * @param id 内容id 视频稿件：视频稿件avid 音频：音频auid 视频合集：视频合集id\n * @param type 内容类型 2：视频稿件 12：音频 21：视频合集\n * @param bvid 视频稿件bvid\n */\n@Serializable\ndata class FavoriteItemId(\n    val id: Long,\n    val type: Int,\n    val bvid: String\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/favorite/Upper.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user.favorite\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 收藏夹上传者\n *\n * @param mid 创建者mid\n * @param name 创建者昵称\n * @param face 创建者头像url\n * @param followed 是否已关注创建者\n * @param vipType 会员类别 0：无 1：月大会员 2：年度及以上大会员\n * @param vipStatue 会员开通状态 0：无 1：有\n */\n@Serializable\ndata class Upper(\n    val mid: Long,\n    val name: String,\n    val face: String,\n    val followed: Boolean = false,\n    @SerialName(\"vip_type\")\n    val vipType: Int = 0,\n    @SerialName(\"vip_statue\")\n    val vipStatue: Int = 0\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/favorite/UserFavoriteFoldersData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user.favorite\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 指定用户创建的所有收藏夹信息\n *\n * @param count 创建的收藏夹总数\n * @param list 创建的收藏夹列表\n */\n@Serializable\ndata class UserFavoriteFoldersData(\n    val count: Int,\n    val list: List<UserFavoriteFolder> = emptyList()\n) {\n    /**\n     * 用户收藏夹信息\n     *\n     * @param id 收藏夹mlid（完整id） 收藏夹原始id+创建者mid尾号2位\n     * @param fid 收藏夹原始id\n     * @param mid 创建者mid\n     * @param attr 属性位（？）\n     * @param title 收藏夹标题\n     * @param favState 目标id是否存在于该收藏夹 存在于该收藏夹：1 不存在于该收藏夹：0\n     * @param mediaCount 收藏夹内容数量\n     */\n    @Serializable\n    data class UserFavoriteFolder(\n        val id: Long,\n        val fid: Long,\n        val mid: Long,\n        val attr: Int,\n        val title: String,\n        @SerialName(\"fav_state\")\n        val favState: Int,\n        @SerialName(\"media_count\")\n        val mediaCount: Int\n    )\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/garb/CardBg.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user.garb\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonObject\n\n@Serializable\ndata class CardBg(\n    @SerialName(\"act_id\")\n    val actId: Int,\n    val level: Int,\n    @SerialName(\"bg_no\")\n    val bgNo: Int,\n    val color: String,\n    @SerialName(\"no_prefix\")\n    val noPrefix: String,\n    @SerialName(\"no_color_format\")\n    val noColorFormat: JsonObject? = null\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/garb/Equip.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user.garb\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonObject\n\n@Serializable\ndata class Equip(\n    val item: Item? = null,\n    val index: Int,\n    val fan: JsonObject? = null,\n    @SerialName(\"is_diy\")\n    val isDiy: Int,\n    @SerialName(\"card_bg\")\n    val cardBg: CardBg? = null,\n    @SerialName(\"previous_item\")\n    val previousItem: Item? = null,\n    @SerialName(\"previous_index\")\n    val previousIndex: Int,\n    @SerialName(\"previous_fan\")\n    val previousFan: JsonObject? = null,\n    @SerialName(\"previous_is_diy\")\n    val previousIsDiy: Int,\n    @SerialName(\"previous_card_bg\")\n    val previousCardBg: CardBg? = null,\n)\n\nenum class EquipPart(val value: String) {\n    Card(\"card\"),\n    CardBg(\"card_bg\"),\n    Loading(\"loading\"),\n    PlayerIcon(\"play_icon\"),\n    Pendant(\"pendant\"),\n    Thumbup(\"thumbup\"),\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/user/garb/Item.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.user.garb\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonObject\n\n@Serializable\ndata class Item(\n    @SerialName(\"item_id\")\n    val itemId: Long,\n    val name: String,\n    val state: String,\n    @SerialName(\"tab_id\")\n    val tabId: Int,\n    val properties: Properties,\n    @SerialName(\"current_activity\")\n    val currentActivity: JsonObject? = null,\n    @SerialName(\"next_activity\")\n    val nextActivity: JsonObject? = null,\n    @SerialName(\"current_sources\")\n    val currentSources: JsonObject? = null,\n    @SerialName(\"next_sources\")\n    val nextSources: JsonObject? = null,\n    @SerialName(\"sale_left_time\")\n    val saleLeftTime: Long,\n    @SerialName(\"sale_time_end\")\n    val saleTimeEnd: Long,\n    @SerialName(\"sale_surplus\")\n    val saleSurplus: Int\n) {\n    /**\n     * 用户装扮属性\n     *\n     * @param dragIcon 进度条动画 - 拖拽 (lottie)\n     * @param dragIconHash 进度条动画 - 拖拽 hash\n     * @param dragLeftPng 进度条动画 - 向左拖拽 (png)\n     * @param dragRightPng 进度条动画 - 向右拖拽 (png)\n     * @param fanNoColor\n     * @param fansImage\n     * @param fansMaterialId\n     * @param garbAvatar 头像框 静态示例\n     * @param goodsType\n     * @param grayRule\n     * @param grayRuleType all\n     * @param hot\n     * @param icon 进度条动画 - 静态 (lottie)\n     * @param iconHash 进度条动画 - idle hash\n     * @param image 预览图片\n     * @param imageAni 预览图片 (点赞) zlib compressed protobuf file\n     * @param imageAniCut 预览图片 (点赞) zlib compressed protobuf file\n     * @param imageAvatar 头像框 示例头像（不包含头像框）\n     * @param imageEnhance 预览图片 动态\n     * @param imageEnhanceFrame 预览图片 帧\n     * @param imagePreview 预览图片\n     * @param imagePreviewSmall 预览图片 (小)\n     * @param loadingFrameUrl 加载动画 帧\n     * @param loadingUrl 加载动画 动图\n     * @param middlePng 进度条动画 - idle (png)\n     * @param realnameAuth\n     * @param saleType other vip_suit suit\n     * @param squaredImage 圆角预览图片\n     * @param staticIconImage 预览图片\n     * @param ver\n     */\n    @Serializable\n    data class Properties(\n        @SerialName(\"drag_icon\")\n        val dragIcon: String? = null,\n        @SerialName(\"drag_icon_hash\")\n        val dragIconHash: String? = null,\n        @SerialName(\"drag_left_png\")\n        val dragLeftPng: String? = null,\n        @SerialName(\"drag_right_png\")\n        val dragRightPng: String? = null,\n        @SerialName(\"fan_no_color\")\n        val fanNoColor: String? = null,\n        @SerialName(\"fans_image\")\n        val fansImage: String? = null,\n        @SerialName(\"fans_material_id\")\n        val fansMaterialId: String? = null,\n        @SerialName(\"garb_avatar\")\n        val garbAvatar: String? = null,\n        @SerialName(\"goods_type\")\n        val goodsType: String? = null,\n        @SerialName(\"gray_rule\")\n        val grayRule: String? = null,\n        @SerialName(\"gray_rule_type\")\n        val grayRuleType: String? = null,\n        val hot: String? = null,\n        val icon: String? = null,\n        @SerialName(\"icon_hash\")\n        val iconHash: String? = null,\n        val image: String? = null,\n        @SerialName(\"image_ani\")\n        val imageAni: String? = null,\n        @SerialName(\"image_ani_cut\")\n        val imageAniCut: String? = null,\n        @SerialName(\"image_avatar\")\n        val imageAvatar: String? = null,\n        @SerialName(\"image_enhance\")\n        val imageEnhance: String? = null,\n        @SerialName(\"image_enhance_frame\")\n        val imageEnhanceFrame: String? = null,\n        @SerialName(\"image_preview\")\n        val imagePreview: String? = null,\n        @SerialName(\"image_preview_small\")\n        val imagePreviewSmall: String? = null,\n        @SerialName(\"loading_frame_url\")\n        val loadingFrameUrl: String? = null,\n        @SerialName(\"loading_url\")\n        val loadingUrl: String? = null,\n        @SerialName(\"middle_png\")\n        val middlePng: String? = null,\n        @SerialName(\"realname_auth\")\n        val realnameAuth: String? = null,\n        @SerialName(\"sale_type\")\n        val saleType: String? = null,\n        @SerialName(\"squared_image\")\n        val squaredImage: String? = null,\n        @SerialName(\"static_icon_image\")\n        val staticIconImage: String? = null,\n        val ver: String? = null\n    ) {\n        fun isLottieDragIcon() = dragIcon != null\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/AddCoin.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.Serializable\n\n/**\n * 投币时顺便点赞的结果\n *\n * @param like 是否点赞成功 true：成功 false：失败 已赞过则附加点赞失败\n */\n@Serializable\ndata class AddCoin(\n    val like: Boolean\n)\n\n/**\n * 检查是否投币\n *\n * @param multiply 投币枚数 未投币为0\n */\n@Serializable\ndata class CheckSentCoin(\n    val multiply:Int\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/ArchiveRelation.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 稿件互动状态\n */\n@Serializable\ndata class ArchiveRelation(\n    val coin: Int = 0,\n    val dislike: Boolean = false,\n    val favorite: Boolean = false,\n    val like: Boolean = false,\n    @SerialName(\"season_fav\")\n    val seasonFav: Boolean = false\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/GaiaVgateData.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GaiaVgateRegisterData(\n    val token: String = \"\",\n    val geetest: GeetestData = GeetestData()\n) {\n    @Serializable\n    data class GeetestData(\n        val gt: String = \"\",\n        val challenge: String = \"\"\n    )\n}\n\n@Serializable\ndata class GaiaVgateValidateData(\n    @SerialName(\"is_valid\")\n    val isValid: Int = 0,\n    @SerialName(\"grisk_id\")\n    val griskId: String = \"\"\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/PlayUrlResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\n\n/**\n * 跳过片头/片尾配置\n *\n * @param materialNo 素材编号\n * @param start DASH分段开始\n * @param end DASH分段结束\n * @param clipType Clip类型 (CLIP_TYPE_OP/CLIP_TYPE_ED/CLIP_TYPE_HE/CLIP_TYPE_MULTI_VIEW/CLIP_TYPE_AD)\n * @param toastText 跳过时的提示语\n * @param multiView 多视图信息\n */\n@Serializable\ndata class ClipInfo(\n    val materialNo: Long = 0L,\n    val start: Int = 0, // 开始时间点，单位秒\n    val end: Int = 0, // 结束时间点，单位秒\n    val clipType: ClipType = ClipType.Unknown,\n    val toastText: String = \"\",\n    val multiView: MultiView? = null\n)\n\n/**\n * 跳过片头/片尾配置: Clip类型\n * JSON 中为字符串值: \"CLIP_TYPE_OP\", \"CLIP_TYPE_ED\" 等\n */\n@Serializable\nenum class ClipType {\n    Unknown,\n    CLIP_TYPE_OP,         // 跳过OP\n    CLIP_TYPE_ED,         // 跳过ED\n    CLIP_TYPE_HE,\n    CLIP_TYPE_MULTI_VIEW,\n    CLIP_TYPE_AD\n}\n\n/**\n * 多视图信息\n */\n@Serializable\ndata class MultiView(\n    val mainViewEndTime: Int? = null,\n    val subViewStartTime: Int? = null\n)\n\n/**\n * @param code 作用尚不明确 pcg独有\n * @param isPreview 作用尚不明确 pcg独有\n * @param fnver 请求时提供的 fnver pcg独有\n * @param fnval 请求时提供的 fnval pcg独有\n * @param videoProject 作用尚不明确 pcg独有\n * @param type 视频流类型（DASH、FLV、MP4） pcg独有\n * @param bp 是否可以承包 pcg独有\n * @param vipType 当前用户大会员类型 pcg独有\n * @param vipStatus 当前用户大会员状态 pcg独有\n * @param isDrm 作用尚不明确 pcg独有\n * @param noRexcode 作用尚不明确 pcg独有\n * @param hasPaid 是否已购买 免费影片始终为 true pcg独有\n * @param status 作用尚不明确 pcg独有\n * @param from local 作用尚不明确\n * @param result suee 作用尚不明确\n * @param message 空 作用尚不明确\n * @param quality 当前的视频分辨率代码 值含义见上表\n * @param format 视频格式\n * @param timeLength 视频长度（毫秒值） 单位为毫秒 不同分辨率/格式可能有略微差异\n * @param acceptFormat 视频支持的全部格式 每项用,分隔\n * @param acceptDescription 视频支持的分辨率列表\n * @param acceptQuality 视频支持的分辨率代码列表 值含义见上表\n * @param videoCodecId 默认选择视频流的编码id 见视频编码代码\n * @param seekParam 固定值：start 作用尚不明确\n * @param seekType offset（dash、flv） second（mp4） 作用尚不明确\n * @param durl 视频分段 注：仅flv/mp4存在此项\n * @param dash dash音视频流信息 注：仅dash存在此项\n * @param supportFormats 支持格式的详细信息\n * @param high_format null\n * @param lastPlayTime 上次播放进度 毫秒值 非pgc接口独有\n * @param lastPlayCid 上次播放分p的cid 非pgc接口独有\n * @param clipInfoList 跳过片头/片尾配置 pcg独有\n * @param recordInfo 备案登记信息 pcg独有\n */\n@Serializable\ndata class PlayUrlData(\n    val code: Int = 0,\n    @SerialName(\"is_preview\")\n    val isPreview: Int = 0,\n    val fnver: Int = 0,\n    val fnval: Int = 0,\n    @SerialName(\"video_project\")\n    val videoProject: Boolean = false,\n    val type: String = \"\",\n    val bp: Int = 0,\n    @SerialName(\"vip_type\")\n    val vipType: Int = 0,\n    @SerialName(\"vip_status\")\n    val vipStatus: Int = 0,\n    @SerialName(\"is_drm\")\n    val isDrm: Boolean = false,\n    @SerialName(\"no_rexcode\")\n    val noRexcode: Int = 0,\n    @SerialName(\"has_paid\")\n    val hasPaid: Boolean = false,\n    val status: Int = 0,\n    val from: String = \"\",\n    val result: String = \"\",\n    val message: String = \"\",\n    val quality: Int = 0,\n    val format: String = \"\",\n    @SerialName(\"timelength\")\n    val timeLength: Int = 0,\n    @SerialName(\"accept_format\")\n    val acceptFormat: String = \"\",\n    @SerialName(\"accept_description\")\n    val acceptDescription: List<String> = emptyList(),\n    @SerialName(\"accept_quality\")\n    val acceptQuality: List<Int> = emptyList(),\n    @SerialName(\"video_codecid\")\n    val videoCodecId: Int = 0,\n    @SerialName(\"seek_param\")\n    val seekParam: String = \"\",\n    @SerialName(\"seek_type\")\n    val seekType: String = \"\",\n    val durl: List<Durl> = emptyList(),\n    val dash: Dash? = null,\n    @SerialName(\"support_formats\")\n    val supportFormats: List<SupportFormat> = emptyList(),\n    @SerialName(\"last_play_time\")\n    val lastPlayTime: Int = 0,\n    @SerialName(\"last_play_cid\")\n    val lastPlaycid: Long = 0,\n    @SerialName(\"clip_info_list\")\n    val clipInfoList: List<ClipInfo> = emptyList(),\n    @SerialName(\"record_info\")\n    val recordInfo: RecordInfo? = null\n)\n\n@Serializable\ndata class PlayUrlV2Data(\n    @SerialName(\"exp_info\")\n    val expInfo: ExpInfo,\n    @SerialName(\"play_check\")\n    val playCheck: PlayCheck,\n    @SerialName(\"play_view_business_info\")\n    val playViewBusinessInfo: PlayViewBusinessInfo,\n    @SerialName(\"video_info\")\n    val videoInfo: PlayUrlData,\n    @SerialName(\"view_info\")\n    val viewInfo: ViewInfo\n) {\n    @Serializable\n    data class ExpInfo(\n        @SerialName(\"buy_vip_donated_season\")\n        val buyVipDonatedSeason: Int\n    )\n\n    @Serializable\n    data class PlayCheck(\n        @SerialName(\"play_detail\")\n        val playDetail: String\n    )\n\n    @Serializable\n    data class PlayViewBusinessInfo(\n        @SerialName(\"episode_info\")\n        val episodeInfo: EpisodeInfo,\n        @SerialName(\"season_info\")\n        val seasonInfo: SeasonInfo,\n        @SerialName(\"user_status\")\n        val userStatus: UserStatus\n    ) {\n        @Serializable\n        data class EpisodeInfo(\n            val aid: Long,\n            val bvid: String,\n            val cid: Long,\n            @SerialName(\"delivery_business_fragment_video\")\n            val deliveryBusinessFragmentVideo: Boolean,\n            @SerialName(\"delivery_fragment_video\")\n            val deliveryFragmentVideo: Boolean,\n            @SerialName(\"ep_id\")\n            val epId: Int,\n            @SerialName(\"ep_status\")\n            val epStatus: Int,\n            val interaction: Interaction,\n            @SerialName(\"long_title\")\n            val longTitle: String,\n            val title: String\n        ) {\n            @Serializable\n            data class Interaction(\n                val interaction: Boolean\n            )\n        }\n\n        @Serializable\n        data class SeasonInfo(\n            @SerialName(\"season_id\")\n            val seasonId: Int,\n            @SerialName(\"season_type\")\n            val seasonType: Int,\n        )\n\n        @Serializable\n        data class UserStatus(\n            @SerialName(\"follow_info\")\n            val followInfo: FollowInfo,\n            @SerialName(\"is_login\")\n            val isLogin: Int,\n            @SerialName(\"pay_info\")\n            val payInfo: PayInfo,\n            @SerialName(\"vip_info\")\n            val vipInfo: VipInfo,\n            @SerialName(\"watch_progress\")\n            val watchProgress: WatchProgress\n        ) {\n            @Serializable\n            data class FollowInfo(\n                val follow: Int,\n                @SerialName(\"follow_status\")\n                val followStatus: Int\n            )\n\n            @Serializable\n            data class PayInfo(\n                @SerialName(\"pay_check\")\n                val payCheck: Int,\n                @SerialName(\"pay_pack_paid\")\n                val payPackPaid: Int,\n                val sponsor: Int\n            )\n\n            @Serializable\n            data class VipInfo(\n                @SerialName(\"real_vip\")\n                val realVip: Boolean\n            )\n\n            @Serializable\n            data class WatchProgress(\n                @SerialName(\"current_watch_progress\")\n                val currentWatchProgress: Int,\n                @SerialName(\"last_ep_id\")\n                val lastEpId: Int,\n                @SerialName(\"last_time\")\n                val lastTime: Int\n            )\n        }\n    }\n\n    @Serializable\n    data class ViewInfo(\n        @SerialName(\"ai_repair_qn_trial_info\")\n        val aiRepairQnTrialInfo: AiRepairQnTrialInfo,\n        @SerialName(\"end_page\")\n        val endPage: EndPage,\n        @SerialName(\"ext_toast\")\n        val extToast: JsonElement,\n        @SerialName(\"qn_trial_info\")\n        val qnTrialInfo: QnTrialInfo,\n        val report: Report\n    ) {\n        @Serializable\n        data class AiRepairQnTrialInfo(\n            @SerialName(\"trial_able\")\n            val trialAble: Boolean\n        )\n\n        @Serializable\n        data class EndPage(\n            val hide: Boolean\n        )\n\n        @Serializable\n        data class QnTrialInfo(\n            @SerialName(\"trial_able\")\n            val trialAble: Boolean\n        )\n\n        @Serializable\n        data class Report(\n            @SerialName(\"ep_id\")\n            val epId: String,\n            @SerialName(\"ep_status\")\n            val epStatus: String,\n            @SerialName(\"season_id\")\n            val seasonId: String,\n            @SerialName(\"season_status\")\n            val seasonStatus: String,\n            @SerialName(\"season_type\")\n            val seasonType: String,\n            @SerialName(\"vip_status\")\n            val vipStatus: String,\n            @SerialName(\"vip_type\")\n            val vipType: String\n        )\n    }\n}\n\n/**\n * 视频播放地址\n *\n * @param order 视频分段序号 某些视频会分为多个片段（从1顺序增长）\n * @param length 视频长度 单位为毫秒\n * @param size 视频大小 单位为Byte\n * @param ahead 空 作用尚不明确\n * @param vhead 空 作用尚不明确\n * @param url 视频流url 注：url内容存在转义符 有效时间为120min\n * @param backupUrl 备用视频流\n */\n@Serializable\ndata class Durl(\n    val order: Int,\n    val length: Int,\n    val size: Int,\n    val ahead: String,\n    val vhead: String,\n    val url: String,\n    @SerialName(\"backup_url\")\n    val backupUrl: List<String> = emptyList()\n)\n\n//TODO\n@Serializable\ndata class Dash(\n    val duration: Int,\n    val minBufferTime: Float,\n    val video: List<DashData> = emptyList(),\n    val audio: List<DashData>? = null,\n    val dolby: DashDolby = DashDolby(),\n    val flac: DashFlac? = null\n)\n\n@Serializable\ndata class DashDolby(\n    val audio: List<DashData>? = null,\n    val type: Int = 2\n)\n\n@Serializable\ndata class DashFlac(\n    val display: Boolean,\n    val audio: DashData? = null\n)\n\n@Serializable\ndata class DashData(\n    val id: Int,\n    @SerialName(\"base_url\")\n    val baseUrl: String,\n    val backupUrl: List<String> = emptyList(),\n    val bandwidth: Int,\n    @SerialName(\"mime_type\")\n    val mimeType: String,\n    val codecs: String,\n    val width: Int,\n    val height: Int,\n    @SerialName(\"frame_rate\")\n    val frameRate: String,\n    val sar: String,\n    @SerialName(\"start_with_sap\")\n    val startWithSap: Int,\n    @SerialName(\"segment_base\")\n    val segmentBase: SegmentBase,\n    @SerialName(\"codecid\")\n    val codecId: Int\n)\n\n@Serializable\ndata class SegmentBase(\n    val initialization: String,\n    @SerialName(\"index_range\")\n    val indexRange: String\n)\n\n/**\n * 支持的视频格式\n *\n * @param quality 视频清晰度代码\n * @param format 视频格式\n * @param newDescription 格式描述\n * @param description 格式描述\n * @param displayDesc 格式描述\n * @param superScript (?)\n * @param codecs 可用编码格式列表\n * @param needLogin 需要登录\n * @param needVip 需要大会员\n */\n@Serializable\ndata class SupportFormat(\n    val quality: Int,\n    val format: String,\n    @SerialName(\"new_description\")\n    val newDescription: String,\n    val description: String? = null,\n    @SerialName(\"display_desc\")\n    val displayDesc: String,\n    @SerialName(\"superscript\")\n    val superScript: String,\n    val codecs: List<String>? = emptyList(),\n    @SerialName(\"need_login\")\n    val needLogin: Boolean = false,\n    @SerialName(\"need_vip\")\n    val needVip: Boolean = false\n)\n\n/**\n * 备案登记信息\n *\n * @param recordIcon\n * @param record 显示文案 登记号：10417060172092207\n */\n@Serializable\ndata class RecordInfo(\n    @SerialName(\"record_icon\")\n    val recordIcon: String,\n    val record: String\n)\n\n@Serializable\nenum class VideoQuality(val qn: Int, val displayName: String) {\n    Q260P(6, \"240P 极速\"),\n    Q360P(16, \"360P 流畅\"),\n    Q480P(32, \"480P 清晰\"),\n    Q720P(64, \"\"),\n    Q720P60(74, \"\"),\n    Q1080P(80, \"\"),\n    Q1080PPlus(112, \"\"),\n    Q1080P60(116, \"\"),\n    Q4K(120, \"\"),\n    HDR(125, \"\"),\n    Dolby(126, \"\"),\n    Q8K(127, \"\")\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/PopularVideosResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PopularVideoData(\n    val list: List<VideoInfo>,\n    @SerialName(\"no_more\")\n    val noMore: Boolean\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/RelatedVideosResponse.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 获取单视频推荐列表\n */\n@Serializable\ndata class RelatedVideosResponse(\n    val code: Int,\n    val message: String,\n    val data: List<RelatedVideoInfo> = emptyList()\n)\n\n/**\n * 视频详细信息\n *\n * @param bvid 稿件bvid\n * @param aid 稿件avid\n * @param videos 稿件分P总数 默认为1\n * @param tid 分区tid\n * @param tname 子分区名称\n * @param copyright 视频类型 1：原创 2：转载\n * @param pic 稿件封面图片url\n * @param title 稿件标题\n * @param pubdate 稿件发布时间 秒级时间戳\n * @param ctime 用户投稿时间 秒级时间戳\n * @param desc 视频简介\n * @param state 视频状态\n * @param duration 稿件总时长(所有分P) 单位为秒\n * @param rights 视频属性标志\n * @param owner 视频UP主信息\n * @param stat 视频状态数\n * @param dynamic 视频同步发布的的动态的文字内容\n * @param cid 视频1P cid\n * @param dimension 视频1P分辨率\n * @param shortLink\n * @param shortLinkV2\n * @param seasonType\n * @param isOgv\n * @param ogvInfo\n * @param rcmdReason 热门推荐理由\n */\n@Serializable\ndata class RelatedVideoInfo(\n    val bvid: String,\n    val aid: Long,\n    val videos: Int,\n    val tid: Int,\n    val tname: String,\n    val copyright: Int,\n    val pic: String,\n    val title: String,\n    val pubdate: Int,\n    val ctime: Int,\n    val desc: String,\n    val state: Int,\n    val duration: Int,\n    val rights: VideoRights,\n    val owner: VideoOwner,\n    val stat: VideoStat,\n    val dynamic: String,\n    val cid: Long,\n    val dimension: Dimension,\n    @SerialName(\"short_link\")\n    val shortLink: String? = null,\n    @SerialName(\"short_link_v2\")\n    val shortLinkV2: String? = null,\n    @SerialName(\"season_type\")\n    val seasonType: Int? = null,\n    @SerialName(\"is_ogv\")\n    val isOgv: Boolean = false,\n    @SerialName(\"ogv_info\")\n    val ogvInfo: String? = null,\n    @SerialName(\"rcmd_reason\")\n    val rcmdReason: String,\n    @SerialName(\"is_charging_archive\")\n    val isChargingArchive: Boolean = false,\n    @SerialName(\"charging_pay\")\n    val chargingPay: ChargingPay? = null\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/SetVideoFavorite.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.Serializable\n\n/**\n * 添加/移除收藏\n *\n * @param prompt 是否为未关注用户收藏 false：否 true：是\n */\n@Serializable\ndata class SetVideoFavorite(\n    val prompt:Boolean\n)\n\n/**\n * 检查是否被收藏\n *\n * @param count 1\n * @param favoured 是否收藏 true：已收藏 false：未收藏\n */\n@Serializable\ndata class CheckVideoFavoured(\n    val count:Int,\n    val favoured:Boolean\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/Tag.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n\n/**\n * 视频标签\n *\n * @param tagId\n * @param tagName TAG名称\n * @param cover TAG图片url\n * @param headCover TAG页面头图url\n * @param content TAG介绍\n * @param shortContent TAG简介\n * @param type\n * @param state\n * @param ctime 创建时间 时间戳\n * @param count 状态数\n * @param isAtten 是否关注 0：未关注 1：已关注 需要登录(Cookie) 未登录为0\n * @param likes\n * @param hates\n * @param attribute\n * @param liked 是否已经点赞 0：未点赞 1：已点赞 需要登录(Cookie) 未登录为0\n * @param hated 是否已经点踩 0：未点踩 1：已点踩 需要登录(Cookie) 未登录为0\n * @param extraAttr\n */\n@Serializable\ndata class Tag(\n    @SerialName(\"tag_id\")\n    val tagId: Int,\n    @SerialName(\"tag_name\")\n    val tagName: String,\n    val cover: String,\n    @SerialName(\"head_cover\")\n    val headCover: String,\n    val content: String,\n    @SerialName(\"short_content\")\n    val shortContent: String,\n    val type: Int,\n    val state: Int,\n    val ctime: Int,\n    val count: Count,\n    @SerialName(\"is_atten\")\n    val isAtten: Int,\n    val likes: Int,\n    val hates: Int,\n    val attribute: Int,\n    val liked: Int,\n    val hated: Int,\n    @SerialName(\"extra_attr\")\n    val extraAttr: Int\n) {\n    /**\n     * Tag 状态数\n     *\n     * @param view\n     * @param use 视频添加TAG数\n     * @param atten TAG关注\n     */\n    @Serializable\n    data class Count(\n        val view: Long,\n        val use: Int,\n        val atten: Int\n    )\n}\n\n/**\n * Tag 详情，包含相似的 Tag 和最新的视频\n *\n * @param info Tag 信息\n * @param similar 相似的 Tag\n * @param news 最新的 Tag 的视频\n */\n@Serializable\ndata class TagDetail(\n    val info: Tag,\n    val similar: List<SimilarTag>,\n    val news: NewTags\n) {\n    /**\n     * 相似的 Tag\n     */\n    @Serializable\n    data class SimilarTag(\n        val rid: Int,\n        val rname: String,\n        val tid: Int,\n        val cover: String,\n        val atten: Int,\n        val tname: String\n    )\n\n    /**\n     * 最新的 Tag 的视频\n     */\n    @Serializable\n    data class NewTags(\n        val count: Int,\n        val archives: List<VideoInfo>\n    )\n}\n\n@Serializable\ndata class TagTopVideosResponse(\n    val code: Int,\n    val message: String,\n    val total: Int,\n    val data: List<VideoInfo>\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/Timeline.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\n\n/**\n * 放送时间表数据（App 端）\n *\n * @param currentTimeText 在当前时间旁边显示的提示语，例如 “会一直在你身边的”，“是追番的friends呢！”\n * @param data 时间表数据\n * @param filter 时间表筛选条件\n * @param isNightMode 是否夜间模式？（我感觉没啥用）\n * @param navigationTitle 导航栏标题\n */\n@Serializable\ndata class TimelineAppData(\n    @SerialName(\"current_time_text\")\n    val currentTimeText: String,\n    val data: List<Timeline>,\n    val filter: List<TimelineFilter>,\n    @SerialName(\"is_night_mode\")\n    val isNightMode: Int,\n    @SerialName(\"navigation_title\")\n    val navigationTitle: String\n)\n\n/**\n * 放送时间表\n *\n * @param date 当日日期\n * @param dateTs 当日日期时间戳\n * @param dayOfWeek 一周中第几天 ∈N∩[1,7]\n * @param dayUpdateText 如果无剧集更新则显示的内容，仅 App 端\n * @param episodes 剧集列表\n * @param isToday 是否今日\n */\n@Serializable\ndata class Timeline(\n    val date: String,\n    @SerialName(\"date_ts\")\n    val dateTs: Int,\n    @SerialName(\"day_of_week\")\n    val dayOfWeek: Int,\n    @SerialName(\"day_update_text\")\n    val dayUpdateText: String? = null,\n    val episodes: List<Episode> = emptyList(),\n    @SerialName(\"is_today\")\n    private val _isToday: Int,\n    @Transient val isToday: Boolean = _isToday == 1\n) {\n    /**\n     * 时间表剧集信息\n     *\n     * @param cover 封面图url\n     * @param delay 是否推迟\n     * @param delayId 推迟一话epid\n     * @param delayIndex 推迟一话名称\n     * @param delayReason 推迟原因\n     * @param epCover 最新一话图url，仅 Web 端\n     * @param enableVt 仅 Web 端\n     * @param episodeId 最新一话的epid\n     * @param follows 仅 Web 端\n     * @param follow 是否已追剧\n     * @param plays 仅 Web 端\n     * @param pubIndex 最新一话名称\n     * @param pubIndexShow 更新最新一话提示语，仅 App 端，例如 “即将更新 第14话”\n     * @param pubTime 发布时间\n     * @param pubTs 发布时间戳 秒\n     * @param published 是否已发布更新\n     * @param report 仅 App 端\n     * @param seasonId 剧集ssid\n     * @param seasonType 剧集类型，仅 App 端\n     * @param squareCover 缩略图url\n     * @param tags 剧集标签，仅 App 端\n     * @param title 剧集标题\n     * @param url 剧集url，仅 App 端\n     */\n    @Serializable\n    data class Episode(\n        val cover: String,\n        val delay: Int,\n        @SerialName(\"delay_id\")\n        val delayId: Int,\n        @SerialName(\"delay_index\")\n        val delayIndex: String,\n        @SerialName(\"delay_reason\")\n        val delayReason: String,\n        @SerialName(\"enable_vt\")\n        val enableVt: Boolean = false,\n        @SerialName(\"ep_cover\")\n        val epCover: String? = null,\n        @SerialName(\"episode_id\")\n        val episodeId: Int,\n        val follows: String? = null,\n        @SerialName(\"follow\")\n        private val _follow: Int? = null,\n        @Transient\n        val follow: Boolean = _follow == 1,\n        val plays: String? = null,\n        @SerialName(\"pub_index\")\n        val pubIndex: String,\n        @SerialName(\"pub_index_show\")\n        val pubIndexShow: String? = null,\n        @SerialName(\"pub_time\")\n        val pubTime: String,\n        @SerialName(\"pub_ts\")\n        val pubTs: Int,\n        @SerialName(\"published\")\n        private val _published: Int,\n        @Transient\n        val published: Boolean = _published == 1,\n        val report: Report? = null,\n        @SerialName(\"season_id\")\n        val seasonId: Int,\n        @SerialName(\"season_type\")\n        val seasonType: Int? = null,\n        @SerialName(\"square_cover\")\n        val squareCover: String,\n        val tags: List<Tag> = emptyList(),\n        val title: String,\n        val url: String? = null\n    ) {\n        @Serializable\n        data class Report(\n            val daynumber: Int,\n            @SerialName(\"ep_id\")\n            val epId: String,\n            @SerialName(\"is_new\")\n            val isNew: String,\n            @SerialName(\"is_published\")\n            val isPublished: String,\n            @SerialName(\"season_id\")\n            val seasonId: Int\n        )\n\n        @Serializable\n        data class Tag(\n            val text: String,\n            val type: Int\n        )\n    }\n}\n\n/**\n * 时间表筛选条件，仅 App 端\n *\n * - 全部: 0\n * - 番剧: 1\n * - 我的追番: 2\n * - 国创: 3\n */\n@Serializable\ndata class TimelineFilter(\n    val desc: String,\n    val type: Int\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/UgcSeason.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\n\n/**\n * 合集信息\n *\n * @param id 合集 id\n * @param title 合集标题\n * @param cover 合集封面\n * @param mid up主 uid\n * @param intro\n * @param signState\n * @param attribute\n * @param sections 合集分集\n */\n@Serializable\ndata class UgcSeason(\n    val id: Int,\n    val title: String,\n    val cover: String,\n    val mid: Long,\n    val intro: String,\n    @SerialName(\"sign_state\")\n    val signState: Int,\n    val attribute: Int,\n    val sections: List<Section>\n) {\n    @Serializable\n    data class Section(\n        @SerialName(\"season_id\")\n        val seasonId: Int,\n        val id: Long,\n        val title: String,\n        val type: Int,\n        val episodes: List<Episode>\n    ) {\n        @Serializable\n        data class Episode(\n            @SerialName(\"season_id\")\n            val seasonId: Int,\n            @SerialName(\"section_id\")\n            val sectionId: Int,\n            val id: Int,\n            val aid: Long,\n            val cid: Long,\n            val title: String,\n            val attribute: Int,\n            val arc: Arc,\n            val page: VideoPage,\n            val bvid: String,\n            val pages: List<VideoPage>\n        ) {\n            @Serializable\n            data class Arc(\n                val aid: Long,\n                val videos: Int,\n                @SerialName(\"type_id\")\n                val typeId: Int,\n                @SerialName(\"type_name\")\n                val typeName: String,\n                val copyright: Int,\n                val pic: String,\n                val title: String,\n                @SerialName(\"pubdate\")\n                val pubDate: Int,\n                val ctime: Int,\n                val desc: String,\n                val state: Int,\n                val duration: Int,\n                val rights: VideoRights,\n                //val author: Author,\n                val stat: VideoStat,\n                val dynamic: String,\n                //val dimension: Dimension,\n                @SerialName(\"desc_v2\")\n                val descV2: JsonElement? = null,\n                @SerialName(\"is_chargeable_season\")\n                val isChargeableSeason: Boolean,\n                @SerialName(\"is_blooper\")\n                val isBlooper: Boolean\n            ) {\n                @Serializable\n                data class Author(\n                    val mid: Long,\n                    val name: String,\n                    val face: String\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/VideoDetail.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport dev.aaa1115910.biliapi.http.entity.user.UserCardData\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonArray\nimport kotlinx.serialization.json.JsonObject\n\n@Serializable\ndata class VideoDetail(\n    @SerialName(\"View\")\n    val view: VideoInfo,\n    @SerialName(\"Card\")\n    val card: UserCardData? = null,\n    @SerialName(\"Tags\")\n    val tags: List<Tag>,\n    //TODO 评论\n    //@SerialName(\"Reply\")\n    //val reply:Any\n    @SerialName(\"Related\")\n    val related: List<RelatedVideoInfo>? = null,\n    @SerialName(\"Spec\")\n    val spec: JsonObject? = null,\n    @SerialName(\"hot_share\")\n    val hotShare: HotShare? = null,\n    val elec: JsonObject? = null,\n    val recommend: JsonObject? = null,\n    @SerialName(\"view_addit\")\n    val viewAddit: JsonObject? = null,\n    val guide: JsonObject? = null,\n    @SerialName(\"query_tags\")\n    val queryTags: JsonObject? = null,\n    //@SerialName(\"is_old_user\")\n    //val isOldUser: Boolean\n) {\n    @Serializable\n    data class HotShare(\n        val show: Boolean,\n        val list: JsonArray\n    )\n\n    /**\n     * 视频 TAG\n     *\n     * @param tagId TAG ID 当存在[musicId]时可能为 0\n     * @param tagName TAG名称\n     * @param musicId 音乐ID\n     * @param tagType TAG类型 bgm old_channel topic\n     * @param jumpUrl 跳转链接\n     */\n    @Serializable\n    data class Tag(\n        @SerialName(\"tag_id\")\n        val tagId: Int,\n        @SerialName(\"tag_name\")\n        val tagName: String,\n        @SerialName(\"music_id\")\n        val musicId: String,\n        @SerialName(\"tag_type\")\n        val tagType: String,\n        @SerialName(\"jump_url\")\n        val jumpUrl: String\n    )\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/VideoInfo.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport dev.aaa1115910.biliapi.http.entity.subtitle.Subtitle\nimport dev.aaa1115910.biliapi.http.entity.user.Staff\nimport dev.aaa1115910.biliapi.http.entity.user.UserGarb\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonElement\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.decodeFromJsonElement\nimport kotlinx.serialization.json.jsonPrimitive\n\n/**\n * 视频详细信息\n *\n * @param bvid 稿件bvid\n * @param aid 稿件avid\n * @param videos 稿件分P总数 默认为1\n * @param tid 分区tid\n * @param tname 子分区名称\n * @param copyright 视频类型 1：原创 2：转载\n * @param pic 稿件封面图片url\n * @param title 稿件标题\n * @param pubdate 稿件发布时间 秒级时间戳\n * @param ctime 用户投稿时间 秒级时间戳\n * @param desc 视频简介\n * @param state 视频状态\n * @param duration 稿件总时长(所有分P) 单位为秒\n * @param forward 撞车视频跳转avid 仅撞车视频存在此字段\n * @param missionId 稿件参与的活动id\n * @param redirectUrl 稿重定向url\t仅番剧或影视视频存在此字段，用于番剧&影视的av/bv->ep\n * @param rights 视频属性标志\n * @param owner 视频UP主信息\n * @param stat 视频状态数\n * @param dynamic 视频同步发布的的动态的文字内容\n * @param cid 视频1P cid\n * @param dimension 视频1P分辨率\n * @param premiere 首播状态\n * @param teenageMode\n * @param isChargeableSeason\n * @param isStory\n * @param noCache\n * @param pages 视频分P列表\n * @param subtitle 视频CC字幕信息\n * @param staff 合作成员列表 非合作视频无此项\n * @param ugcSeason 合集信息\n * @param isSeasonDisplay 是否为合集\n * @param userGarb 用户装扮信息\n * @param honorReply\n * @param likeIcon\n * @param shortLink\n * @param shortLinkV2\n * @param firstFrame\n * @param pubLocation\n * @param seasonType\n * @param isOgv\n * @param ogvInfo\n * @param rcmdReason 热门推荐理由\n */\n@Serializable\ndata class VideoInfo(\n    val bvid: String,\n    val aid: Long,\n    val videos: Int,\n    val tid: Int,\n    val tname: String,\n    val copyright: Int,\n    val pic: String,\n    val title: String,\n    val pubdate: Int,\n    val ctime: Int = 0,\n    val desc: String,\n    val state: Int,\n    val duration: Int,\n    val forward: Int? = null,\n    @SerialName(\"mission_id\")\n    val missionId: Int? = null,\n    @SerialName(\"redirect_url\")\n    val redirectUrl: String? = null,\n    val rights: VideoRights,\n    val owner: VideoOwner,\n    val stat: VideoStat,\n    val dynamic: String,\n    val cid: Long,\n    val dimension: Dimension,\n    val premiere: Premiere? = null,\n    @SerialName(\"teenage_mode\")\n    val teenageMode: Int = 0,\n    @SerialName(\"is_chargeable_season\")\n    val isChargeableSeason: Boolean = false,\n    @SerialName(\"is_story\")\n    val isStory: Boolean = false,\n    @SerialName(\"no_cache\")\n    val noCache: Boolean = false,\n    val pages: List<VideoPage> = emptyList(),\n    val subtitle: Subtitle? = null,\n    val staff: List<Staff> = emptyList(),\n    @SerialName(\"ugc_season\")\n    val ugcSeason: UgcSeason? = null,\n    @SerialName(\"is_season_display\")\n    val isSeasonDisplay: Boolean = false,\n    @SerialName(\"user_garb\")\n    val userGarb: UserGarb? = null,\n    @SerialName(\"honor_reply\")\n    val honorReply: HonorReply? = null,\n    @SerialName(\"like_icon\")\n    val likeIcon: String? = null,\n    @SerialName(\"short_link\")\n    val shortLink: String? = null,\n    @SerialName(\"short_link_v2\")\n    val shortLinkV2: String? = null,\n    @SerialName(\"first_frame\")\n    val firstFrame: String? = null,\n    @SerialName(\"pub_location\")\n    val pubLocation: String? = null,\n    @SerialName(\"season_type\")\n    val seasonType: Int? = null,\n    @SerialName(\"is_ogv\")\n    val isOgv: Boolean = false,\n    @SerialName(\"ogv_info\")\n    val ogvInfo: String? = null,\n    @SerialName(\"rcmd_reason\")\n    private val _rcmdReason: JsonElement? = null,\n    var rcmdReason: RcmdReason? = null,\n    @SerialName(\"is_upower_exclusive\")\n    val isUpowerExclusive: Boolean = false,\n    @SerialName(\"is_upower_play\")\n    val isUpowerPlay: Boolean = false\n) {\n    init {\n        rcmdReason = if (_rcmdReason == null) {\n            null\n        } else if (_rcmdReason is JsonObject) {\n            Json.decodeFromJsonElement<RcmdReason>(_rcmdReason)\n        } else {\n            val reason = _rcmdReason.jsonPrimitive.content\n            if (reason == \"\") null else RcmdReason(content = reason, cornerMark = 0)\n        }\n    }\n\n    @Serializable\n    data class RcmdReason(\n        val content: String,\n        @SerialName(\"corner_mark\")\n        val cornerMark: Int\n    )\n}\n\n/**\n * 视频属性标志\n *\n * @param bp 是否允许承包\n * @param elec 是否支持充电\n * @param download 是否允许下载\n * @param movie 是否电影\n * @param pay 是否PGC付费\n * @param hd5 是否有高码率\n * @param noReprint 是否显示“禁止转载”标志\n * @param autoplay 是否自动播放\n * @param ugcPay 是否UGC付费\n * @param isCooperation 是否为联合投稿\n * @param ugcPayPreview\n * @param noBackground\n * @param cleanMode\n * @param isSteinGate 是否为互动视频\n * @param is360 是否为全景视频\n * @param noShare\n * @param arcPay\n * @param payFreeWatch\n */\n@Serializable\ndata class VideoRights(\n    val bp: Int,\n    val elec: Int,\n    val download: Int,\n    val movie: Int,\n    val pay: Int,\n    val hd5: Int,\n    @SerialName(\"no_reprint\")\n    val noReprint: Int,\n    val autoplay: Int,\n    @SerialName(\"ugc_pay\")\n    val ugcPay: Int,\n    @SerialName(\"is_cooperation\")\n    val isCooperation: Int,\n    @SerialName(\"ugc_pay_preview\")\n    val ugcPayPreview: Int,\n    @SerialName(\"no_background\")\n    val noBackground: Int? = null,\n    @SerialName(\"clean_mode\")\n    val cleanMode: Int? = null,\n    @SerialName(\"is_stein_gate\")\n    val isSteinGate: Int? = null,\n    @SerialName(\"is_360\")\n    val is360: Int? = null,\n    @SerialName(\"no_share\")\n    val noShare: Int? = null,\n    @SerialName(\"arc_pay\")\n    val arcPay: Int,\n    @SerialName(\"pay_free_watch\")\n    val payFreeWatch: Int? = null\n)\n\n\n/**\n * 视频作者\n *\n * @param mid UP主mid\n * @param name UP主昵称\n * @param face UP主头像\n */\n@Serializable\ndata class VideoOwner(\n    val mid: Long,\n    val name: String,\n    val face: String\n)\n\n\n/**\n * 视频数据\n *\n * @param aid 稿件avid\n * @param view 播放数\n * @param danmaku 弹幕数\n * @param reply 评论数\n * @param favorite 收藏数\n * @param coin 投币数\n * @param share 分享数\n * @param nowRank 当前排名\n * @param hisRank 历史最高排行\n * @param like 获赞数\n * @param dislike 点踩数\t恒为0\n * @param evaluation 视频评分\n * @param argueMsg 警告/争议提示信息\n */\n@Serializable\ndata class VideoStat(\n    val aid: Long = 0,\n    val view: Long = 0,\n    val danmaku: Int = 0,\n    val reply: Int = 0,\n    val favorite: Int = 0,\n    val coin: Int = 0,\n    val share: Int = 0,\n    @SerialName(\"now_rank\")\n    val nowRank: Int = 0,\n    @SerialName(\"his_rank\")\n    val hisRank: Int = 0,\n    val like: Int = 0,\n    val dislike: Int = 0,\n    val evaluation: String = \"\",\n    @SerialName(\"argue_msg\")\n    val argueMsg: String = \"\"\n)\n\n/**\n * 分辨率\n *\n * @param width 当前分P 宽度\n * @param height 当前分P 高度\n * @param rotate 是否将宽高对换 0：正常,1：对换\n */\n@Serializable\ndata class Dimension(\n    val width: Int,\n    val height: Int,\n    val rotate: Int\n)\n\n@Serializable\ndata class Premiere(\n    val state: Int,\n    @SerialName(\"start_time\")\n    val startTime: Long,\n    @SerialName(\"room_id\")\n    val roomId: Int\n)\n\n/**\n * 视频分P\n *\n * @param cid 分P cid\n * @param page 分P序号 从1开始\n * @param from 视频来源 vupload：普通上传（B站） hunan：芒果TV qq：腾讯\n * @param part 分P标题\n * @param duration 分P持续时间 单位为秒\n * @param vid 站外视频vid\t仅站外视频有效\n * @param weblink 站外视频跳转url 仅站外视频有效\n * @param dimension 当前分P分辨率 部分较老视频无分辨率值\n */\n@Serializable\ndata class VideoPage(\n    val cid: Long,\n    val page: Int,\n    val from: String,\n    val part: String,\n    val duration: Int,\n    val vid: String,\n    val weblink: String,\n    val dimension: Dimension\n)\n\n/**\n * 推荐理由\n *\n * @param honor\n */\n@Serializable\ndata class HonorReply(\n    val honor: List<HonorReplyItem> = emptyList()\n)\n\n/**\n * 推荐信息\n *\n * @param aid 当前稿件aid\n * @param type 2：第?期每周必看 3：全站排行榜最高第?名 4：热门\n * @param desc 描述\n * @param weeklyRecommendNum\n */\n@Serializable\ndata class HonorReplyItem(\n    val aid: Long,\n    val type: Int,\n    val desc: String,\n    @SerialName(\"weekly_recommend_num\")\n    val weeklyRecommendNum: Int\n)\n\n@Serializable\ndata class ChargingPay(\n    val level: Int = 0\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/VideoMoreInfo.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport dev.aaa1115910.biliapi.http.entity.user.LevelInfo\nimport dev.aaa1115910.biliapi.http.entity.user.Vip\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonArray\n\n/**\n * 视频更多信息\n *\n * @param aid avid\n * @param bvid bvid\n * @param allowBp\n * @param noShare\n * @param cid cid\n * @param maxLimit\n * @param pageNo\n * @param hasNext\n * @param ipInfo IP 地址信息\n * @param loginMid 当前用户 mid\n * @param loginMidHash 当前用户 mid hash\n * @param isOwner\n * @param name 用户昵称\n * @param permission 用户权限，以逗号分隔\n * @param levelInfo 用户等级信息\n * @param vip 大会员信息\n * @param answerStatue\n * @param blockTime\n * @param role\n * @param lastPlayTime 最后播放进度，单位毫秒\n * @param lastPlayCid 最后播放分 P 的 cid\n * @param nowTime 当天时间，单位秒\n * @param onlineCount 所有终端总计在线人数\n * @param dmMask\n * @param subtitle\n * @param playerIcon\n * @param viewPoints\n * @param isUgcPayPreview\n * @param previewToast\n * @param pcdnLoader\n * @param options\n * @param guideAttention\n * @param jumpCard\n * @param operationCard\n * @param onlineSwitch\n * @param fawkes\n * @param showSwitch\n * @param toastBlock\n */\n@Serializable\ndata class VideoMoreInfo(\n    val aid: Long,\n    val bvid: String,\n    @SerialName(\"allow_bp\")\n    val allowBp: Boolean,\n    @SerialName(\"no_share\")\n    val noShare: Boolean,\n    val cid: Long,\n    @SerialName(\"max_limit\")\n    val maxLimit: Int,\n    @SerialName(\"page_no\")\n    val pageNo: Int,\n    @SerialName(\"has_next\")\n    val hasNext: Boolean,\n    @SerialName(\"ip_info\")\n    val ipInfo: IpInfo,\n    @SerialName(\"login_mid\")\n    val loginMid: Long,\n    @SerialName(\"login_mid_hash\")\n    val loginMidHash: String,\n    @SerialName(\"is_owner\")\n    val isOwner: Boolean,\n    val name: String,\n    val permission: String,\n    @SerialName(\"level_info\")\n    val levelInfo: LevelInfo,\n    val vip: Vip,\n    @SerialName(\"answer_status\")\n    val answerStatue: Int,\n    @SerialName(\"block_time\")\n    val blockTime: Int,\n    val role: String,\n    @SerialName(\"last_play_time\")\n    val lastPlayTime: Int,\n    @SerialName(\"last_play_cid\")\n    val lastPlayCid: Long,\n    @SerialName(\"now_time\")\n    val nowTime: Int,\n    @SerialName(\"online_count\")\n    val onlineCount: Int,\n    @SerialName(\"dm_mask\")\n    val dmMask: DmMask? = null,\n    val subtitle: Subtitle? = null,\n    @SerialName(\"player_icon\")\n    val playerIcon: PlayerIcon? = null,\n    @SerialName(\"view_points\")\n    val viewPoints: JsonArray,\n    @SerialName(\"is_ugc_pay_preview\")\n    val isUgcPayPreview: Boolean,\n    @SerialName(\"preview_toast\")\n    val previewToast: String,\n    @SerialName(\"pcdn_loader\")\n    val pcdnLoader: PcdnLoader? = null,\n    val options: Options,\n    @SerialName(\"guide_attention\")\n    val guideAttention: JsonArray,\n    @SerialName(\"jump_card\")\n    val jumpCard: JsonArray,\n    @SerialName(\"operation_card\")\n    val operationCard: JsonArray,\n    @SerialName(\"online_switch\")\n    val onlineSwitch: OnlineSwitch,\n    val fawkes: Fawkes,\n    @SerialName(\"show_switch\")\n    val showSwitch: ShowSwitch,\n    //@SerialName(\"bgm_info\")\n    //val bgmInfo: Any\n    @SerialName(\"toast_block\")\n    val toastBlock: Boolean\n) {\n    /**\n     * IP 信息\n     *\n     * @param ip IP 地址\n     * @param zoneIp\n     * @param zoneId\n     * @param country 国家\n     * @param province 省份\n     * @param city 城市\n     */\n    @Serializable\n    data class IpInfo(\n        val ip: String,\n        @SerialName(\"zone_ip\")\n        val zoneIp: String,\n        @SerialName(\"zone_id\")\n        val zoneId: Int,\n        val country: String,\n        val province: String,\n        val city: String\n    )\n\n    /**\n     * @param cid\n     * @param plat\n     * @param fps\n     * @param time\n     * @param maskUrl\n     */\n    @Serializable\n    data class DmMask(\n        val cid: Long,\n        val plat: Int,\n        val fps: Int,\n        val time: Int,\n        @SerialName(\"mask_url\")\n        val maskUrl: String\n    )\n\n    /**\n     * @param allowSubmit 允许提交字幕\n     * @param lan\n     * @param lanDoc\n     * @param subtitles\n     */\n    @Serializable\n    data class Subtitle(\n        @SerialName(\"allow_submit\")\n        val allowSubmit: Boolean,\n        val lan: String,\n        @SerialName(\"lan_doc\")\n        val lanDoc: String,\n        val subtitles: List<SubtitleItem> = emptyList()\n    )\n\n    /**\n     * 字幕信息\n     *\n     * @param id\n     * @param lan 字幕语言代号，例如zh-Hans\n     * @param lanDoc 字幕语言名称\n     * @param isLock\n     * @param subtitleUrl\n     * @param type\n     * @param idStr\n     * @param aiType\n     * @param aiStatus\n     */\n    @Serializable\n    data class SubtitleItem(\n        val id: Long,\n        val lan: String,\n        @SerialName(\"lan_doc\")\n        val lanDoc: String,\n        @SerialName(\"is_lock\")\n        val isLock: Boolean,\n        @SerialName(\"subtitle_url\")\n        val subtitleUrl: String,\n        val type: Int,\n        @SerialName(\"id_str\")\n        val idStr: String,\n        @SerialName(\"ai_type\")\n        val aiType: Int,\n        @SerialName(\"ai_status\")\n        val aiStatus: Int\n    )\n\n    /**\n     * @param url1\n     * @param hash1\n     * @param url2\n     * @param hash2\n     * @param ctime\n     */\n    @Serializable\n    data class PlayerIcon(\n        val url1: String? = null,\n        val hash1: String? = null,\n        val url2: String? = null,\n        val hash2: String? = null,\n        val ctime: Int? = null\n    )\n\n    /**\n     * @param flv\n     * @param dash\n     */\n    @Serializable\n    data class PcdnLoader(\n        val flv: PcdnLoaderItem,\n        val dash: PcdnLoaderItem\n    ) {\n        /**\n         * @param group\n         * @param labels\n         */\n        @Serializable\n        data class PcdnLoaderItem(\n            val group: String? = null,\n            val labels: Labels\n        ) {\n            /**\n             * @param pcdnVideoType\n             * @param pcdnStage\n             * @param pcdnGroup\n             */\n            @Serializable\n            data class Labels(\n                @SerialName(\"pcdn_video_type\")\n                val pcdnVideoType: String,\n                @SerialName(\"pcdn_stage\")\n                val pcdnStage: String,\n                @SerialName(\"pcdn_group\")\n                val pcdnGroup: String\n            )\n        }\n    }\n\n    /**\n     * @param is360\n     * @param withoutVip\n     */\n    @Serializable\n    data class Options(\n        @SerialName(\"is_360\")\n        val is360: Boolean,\n        @SerialName(\"without_vip\")\n        val withoutVip: Boolean\n    )\n\n    /**\n     * @param enableGrayDashPlayback\n     * @param newBroadcast\n     * @param realtimeDm\n     * @param subtitleSubmitSwitch\n     */\n    @Serializable\n    data class OnlineSwitch(\n        @SerialName(\"enable_gray_dash_playback\")\n        val enableGrayDashPlayback: String,\n        @SerialName(\"new_broadcast\")\n        val newBroadcast: String,\n        @SerialName(\"realtime_dm\")\n        val realtimeDm: String,\n        @SerialName(\"subtitle_submit_switch\")\n        val subtitleSubmitSwitch: String\n    )\n\n    /**\n     * @param configVersion\n     * @param ffVersion\n     */\n    @Serializable\n    data class Fawkes(\n        @SerialName(\"config_version\")\n        val configVersion: Int,\n        @SerialName(\"ff_version\")\n        val ffVersion: Int\n    )\n\n    /**\n     * @param longProgress\n     */\n    @Serializable\n    data class ShowSwitch(\n        @SerialName(\"long_progress\")\n        val longProgress: Boolean\n    )\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/VideoOnlineTotal.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * 视频在线观看人数\n *\n * @param total 在线观看人数（字符串格式）\n */\n@Serializable\ndata class VideoOnlineTotal(\n    @SerialName(\"total\")\n    val total: String\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/video/VideoShot.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.video\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\n\n@Serializable\ndata class VideoShot(\n    @SerialName(\"pvdata\")\n    val pvData: String? = null,\n    @SerialName(\"img_x_len\")\n    val imgXLen: Int = 10,\n    @SerialName(\"img_y_len\")\n    val imgYLen: Int = 10,\n    @SerialName(\"img_x_size\")\n    val imgXSize: Int = 0,\n    @SerialName(\"img_y_size\")\n    val imgYSize: Int = 0,\n    val image: List<String> = emptyList(),\n    val index: List<UShort>? = null,\n    @SerialName(\"video_shots\")\n    var videoShots: JsonElement? = null,\n    var indexs: JsonElement? = null\n)\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/web/Hover.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.web\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Hover(\n    val text: List<String>,\n    val img: String\n)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/entity/web/Nav.kt",
    "content": "package dev.aaa1115910.biliapi.http.entity.web\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class NavResponseData(\n    val isLogin: Boolean,\n    @SerialName(\"wbi_img\")\n    val wbiImg: WbiImg\n) {\n    @Serializable\n    data class WbiImg(\n        @SerialName(\"img_url\")\n        val imgUrl: String,\n        @SerialName(\"sub_url\")\n        val subUrl: String\n    ) {\n        fun getImgKey(): String = imgUrl.split(\"/\").last().split(\".\").first()\n        fun getSubKey(): String = subUrl.split(\"/\").last().split(\".\").first()\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/plugins/BiliUserAgent.kt",
    "content": "package dev.aaa1115910.biliapi.http.plugins\n\nimport dev.aaa1115910.biliapi.http.util.BiliAppConf\nimport dev.aaa1115910.biliapi.http.util.BiliWebConf\nimport io.ktor.client.HttpClientConfig\nimport io.ktor.client.plugins.api.ClientPlugin\nimport io.ktor.client.plugins.api.createClientPlugin\nimport io.ktor.client.request.header\nimport io.ktor.client.request.host\nimport io.ktor.http.HttpHeaders\nimport io.ktor.util.logging.KtorSimpleLogger\nimport io.ktor.utils.io.KtorDsl\n\nprivate val LOGGER = KtorSimpleLogger(\"dev.aaa1115910.biliapi.http.plugins.BiliUserAgent\")\n\n@KtorDsl\nclass BiliUserAgentConfig(\n    var version: String = BiliAppConf.APP_VERSION_NAME,\n    var buildCode: Int = BiliAppConf.APP_BUILD_CODE,\n    var channel: String = BiliAppConf.CHANNEL,\n    var platform: String = BiliAppConf.PLATFORM,\n    var mobiApp: String = BiliAppConf.MOBI_APP,\n    var model: String = BiliAppConf.model,\n    var osVersion: String = BiliAppConf.osVersion,\n    var network: Int = BiliAppConf.NETWORK,\n    var webViewVersion: Int = BiliWebConf.webViewVersion\n) {\n    var appUserAgent = \"\"\n        private set\n    var webUserAgent = \"\"\n        private set\n\n    fun buildUserAgents() {\n        appUserAgent =\n            \"Mozilla/5.0 BiliDroid/$version (bbcallen@gmail.com) os/$platform model/$model mobi_app/$mobiApp build/$buildCode channel/$channel innerVer/$buildCode osVer/$osVersion network/$network\"\n        webUserAgent =\n            \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$webViewVersion.0.0.0 Safari/537.36\"\n    }\n}\n\nval BiliUserAgent: ClientPlugin<BiliUserAgentConfig> =\n    createClientPlugin(\"BiliUserAgent\", ::BiliUserAgentConfig) {\n        pluginConfig.buildUserAgents()\n        val appUserAgent = pluginConfig.appUserAgent\n        val webUserAgent = pluginConfig.webUserAgent\n        onRequest { request, _ ->\n            val userAgent =\n                if (request.host == \"app.bilibili.com\" || request.host == \"passport.bilibili.com\") {\n                    appUserAgent\n                } else {\n                    webUserAgent\n                }\n            LOGGER.trace(\"Adding User-Agent header: agent \\\"${userAgent}\\\" for ${request.url}\")\n            request.header(HttpHeaders.UserAgent, userAgent)\n        }\n    }\n\n@Suppress(\"FunctionName\")\nfun HttpClientConfig<*>.BiliUserAgent() {\n    install(BiliUserAgent) {\n\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/util/ApiSign.kt",
    "content": "package dev.aaa1115910.biliapi.http.util\n\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport io.ktor.client.HttpClient\nimport io.ktor.client.plugins.HttpSend\nimport io.ktor.client.plugins.plugin\nimport io.ktor.client.request.HttpRequestBuilder\nimport io.ktor.client.request.forms.FormDataContent\nimport io.ktor.client.request.parameter\nimport io.ktor.client.request.setBody\nimport io.ktor.client.utils.EmptyContent\nimport io.ktor.http.HttpMethod\nimport io.ktor.http.Parameters\nimport io.ktor.http.URLBuilder\nimport io.ktor.http.clone\nimport io.ktor.http.encodedPath\nimport io.ktor.http.plus\nimport io.ktor.util.AttributeKey\nimport java.net.URLEncoder\nimport java.security.MessageDigest\n\nprivate val SkipAddBuvid3CookieKey = AttributeKey<Boolean>(\"SkipAddBuvid3Cookie\")\n\nfun HttpRequestBuilder.skipAddBuvid3Cookie() {\n    attributes.put(SkipAddBuvid3CookieKey, true)\n}\n\nprivate const val APP_KEY = \"dfca71928277209b\"\nprivate const val APP_SEC = \"b5475a8825547a4fc26c7d518eaaa02e\"\nprivate val mixinKeyEncTab = listOf(\n    46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,\n    33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61,\n    26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36,\n    20, 34, 44, 52\n)\n\nfun HttpRequestBuilder.encAppPost() {\n    var parameters = (body as FormDataContent).formData\n    parameters += Parameters.build { append(\"appkey\", APP_KEY) }\n\n    val sortedParams = parameters.entries()\n        .associate { it.key to it.value.first() }\n        .toSortedMap()\n        .map { (key, value) -> \"$key=${URLEncoder.encode(value, \"utf-8\")}\" }\n        .joinToString(\"&\")\n\n    val sign = MessageDigest.getInstance(\"MD5\").digest((sortedParams + APP_SEC).toByteArray())\n        .joinToString(\"\") { \"%02x\".format(it) }\n\n    parameters += Parameters.build { append(\"sign\", sign) }\n    setBody(FormDataContent(parameters))\n    println(\"sign: $sign\")\n}\n\nfun HttpRequestBuilder.encAppGet() {\n    parameter(\"appkey\", APP_KEY)\n\n    val sortedParams = url.encodedParameters.entries()\n        .associate { it.key to it.value.first() }\n        .toSortedMap()\n        .also {\n            url.parameters.clear()\n            it.entries.forEach { (key, value) -> parameter(key, value) }\n        }\n\n    val sortedParamsString = sortedParams\n        .map { (key, value) -> \"$key=$value\" }\n        .joinToString(\"&\")\n\n    val sign = MessageDigest.getInstance(\"MD5\").digest((sortedParamsString + APP_SEC).toByteArray())\n        .joinToString(\"\") { \"%02x\".format(it) }\n\n    parameter(\"sign\", sign)\n    println(\"sign: $sign\")\n}\n\nsuspend fun HttpRequestBuilder.encWbi() {\n    val getMixinKey: (orig: String) -> String = { orig ->\n        val mixinKey = mixinKeyEncTab.fold(\"\") { s, i -> s + orig[i] }\n        mixinKey.substring(0, 32)\n    }\n\n    if (BiliHttpApi.wbiImgKey == null || BiliHttpApi.wbiSubKey == null) BiliHttpApi.updateWbi()\n    require(BiliHttpApi.wbiImgKey != null && BiliHttpApi.wbiSubKey != null) { \"Wbi keys can't be null!\" }\n    val mixinKey = getMixinKey(BiliHttpApi.wbiImgKey + BiliHttpApi.wbiSubKey)\n\n    val wts = (System.currentTimeMillis() / 1000).toInt()\n    parameter(\"wts\", wts)\n\n    val sortedParams = url.encodedParameters.entries()\n        .associate { it.key to it.value.first() }\n        .toSortedMap()\n        .map { (key, value) ->\n            // 过滤特殊字符 !\"!'()*\n            val filteredValue = value.filter { c -> c !in setOf('!', '\\'', '(', ')', '*') }\n            \"$key=$filteredValue\"\n        }\n        .joinToString(\"&\")\n\n    val wRid = MessageDigest.getInstance(\"MD5\").digest((sortedParams + mixinKey).toByteArray())\n        .joinToString(\"\") { \"%02x\".format(it) }\n    parameter(\"w_rid\", wRid)\n}\n\nfun HttpClient.encApiSign() = plugin(HttpSend)\n    .intercept { request ->\n        // skip when using grpc proxy\n        if (request.url.encodedPath.startsWith(\"bilibili.\")) {\n            return@intercept execute(request)\n        }\n\n        val getUrlWithoutAccessToken: (URLBuilder) -> String = { urlBuilder ->\n            urlBuilder.clone().apply {\n                if (parameters.contains(\"access_key\") && !parameters[\"access_key\"].isNullOrBlank()) {\n                    parameters[\"access_key\"] = \"HIDDEN_ACCESS_TOKEN\"\n                }\n            }.toString()\n        }\n\n        // 为 Web 请求自动添加 buvid3 cookie\n        val isAppRequest =\n            request.url.parameters.contains(\"access_key\") || request.url.host == \"app.bilibili.com\"\n        val isSkipBuvid3Cookie = request.attributes.getOrNull(SkipAddBuvid3CookieKey) == true\n        if (!isAppRequest && !isSkipBuvid3Cookie) {\n            val buvid3 = BiliHttpApi.buvid3Provider()\n            val existingCookie = request.headers[\"Cookie\"] ?: \"\"\n            if (!buvid3.isNullOrBlank() && !existingCookie.contains(\"buvid3=\")) {\n                val newCookie = if (existingCookie.isNotBlank()) {\n                    \"buvid3=$buvid3; $existingCookie\"\n                } else {\n                    \"buvid3=$buvid3\"\n                }\n                request.headers[\"Cookie\"] = newCookie\n            }\n        }\n\n        when (request.method) {\n            // app 端如果既用到了 wbi get 接口，也用到了 token 去请求，那是先计算 wbi sign 还是 app sign？\n            // 目前看来需要计算 wbi sign 的接口之前忘记计算 app sign 都通过校验了🤯\n            HttpMethod.Get -> {\n                val isWbiRequest = request.url.encodedPath.contains(\"wbi\") ||\n                        request.url.encodedPath.contains(\"/pgc/player/web/playurl\") ||\n                        request.url.encodedPath.contains(\"/pgc/player/web/v2/playurl\")\n                if (isWbiRequest) {\n                    println(\"Enc wbi for get request: ${getUrlWithoutAccessToken(request.url)}\")\n                    request.encWbi()\n                } else if (isAppRequest) {\n                    println(\"Enc app sign for get request: ${getUrlWithoutAccessToken(request.url)}\")\n                    request.encAppGet()\n                    println(getUrlWithoutAccessToken(request.url))\n                }\n            }\n\n            HttpMethod.Post -> {\n                if (request.body is EmptyContent) return@intercept execute(request)\n                val parameters = (request.body as FormDataContent).formData\n                val isParametersContainKeywords = parameters.contains(\"access_key\")\n                val isPathContainKeywords = request.url.encodedPath.contains(\"passport\")\n                if (isParametersContainKeywords || isPathContainKeywords) {\n                    println(\"Enc app sign for post request: ${getUrlWithoutAccessToken(request.url)}\")\n                    request.encAppPost()\n                }\n            }\n        }\n        execute(request)\n    }\n\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/util/BiliAppConf.kt",
    "content": "package dev.aaa1115910.biliapi.http.util\n\nobject BiliAppConf {\n    const val GRPC_HOST = \"grpc.biliapi.net\"\n    const val GRPC_PORT = 443\n    const val APP_ID = 5\n    const val APP_BUILD_CODE = 2020100\n    const val APP_VERSION_NAME = \"2.2.0\"\n    const val CHANNEL = \"yingyongbao\"\n    const val MOBI_APP = \"android_hd\"\n    const val DEVICE = \"\"\n    const val PLATFORM = \"android\"\n    const val TIMEZONE = \"Asia/Shanghai\"\n    const val NETWORK = 2\n    var osVersion = \"15\"\n    var model = \"2505DRP06G\"\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/util/BiliDns.kt",
    "content": "package dev.aaa1115910.biliapi.http.util\n\nimport okhttp3.Dns\nimport java.net.Inet4Address\nimport java.net.InetAddress\n\nobject BiliDns : Dns {\n    var ipv4Only: Boolean = false\n\n    override fun lookup(hostname: String): List<InetAddress> {\n        val addresses = Dns.SYSTEM.lookup(hostname)\n        if (!ipv4Only) return addresses\n        val ipv4Addresses = addresses.filter { it is Inet4Address }\n        return ipv4Addresses.ifEmpty { addresses }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/util/BiliWebConf.kt",
    "content": "package dev.aaa1115910.biliapi.http.util\n\nobject BiliWebConf {\n    var webViewVersion = 144\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/util/Brotli.kt",
    "content": "package dev.aaa1115910.biliapi.http.util\n\nimport org.brotli.dec.BrotliInputStream\nimport java.io.ByteArrayInputStream\nimport java.io.ByteArrayOutputStream\n\nfun ByteArray.brotliDecompress(): ByteArray {\n    val inputStream = BrotliInputStream(ByteArrayInputStream(this))\n    // 预估解压后大小，减少扩容次数\n    val estimatedSize = this.size * 4\n    val outputStream = ByteArrayOutputStream(estimatedSize.coerceAtLeast(4096))\n    return outputStream.use {\n        // 增大缓冲区，减少循环次数\n        val buffer = ByteArray(8192)\n        var count: Int\n        try {\n            while (inputStream.read(buffer).also { count = it } != -1) {\n                outputStream.write(buffer, 0, count)\n            }\n        } finally {\n            inputStream.close()\n        }\n        outputStream.toByteArray()\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/util/Buvid.kt",
    "content": "package dev.aaa1115910.biliapi.http.util\n\nimport java.math.BigInteger\nimport java.security.MessageDigest\n\nfun generateBuvid(): String {\n    val mac = mutableListOf<String>()\n    for (i in 0 until 6) {\n        val min = 0\n        val max = 0xff\n        val num = (Math.random() * (max - min + 1) + min).toInt().toString(16)\n        mac.add(num)\n    }\n    val md5 = md5(mac.joinToString(\":\"))\n    val md5Arr = md5.split(\"\").toTypedArray()\n    return \"XY${md5Arr[2]}${md5Arr[12]}${md5Arr[22]}$md5\"\n}\n\nfun md5(input: String): String {\n    val md = MessageDigest.getInstance(\"MD5\")\n    val messageDigest = md.digest(input.toByteArray())\n    val no = BigInteger(1, messageDigest)\n    var hashText = no.toString(16)\n    while (hashText.length < 32) {\n        hashText = \"0$hashText\"\n    }\n    return hashText\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/util/CommonEnumIntSerializer.kt",
    "content": "package dev.aaa1115910.biliapi.http.util\n\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.descriptors.SerialDescriptor\nimport kotlinx.serialization.descriptors.serialDescriptor\nimport kotlinx.serialization.encoding.Decoder\nimport kotlinx.serialization.encoding.Encoder\n\n@Suppress(\"MemberVisibilityCanBePrivate\")\nopen class CommonEnumIntSerializer<T>(val serialName: String, val choices: Array<T>, val choicesNumbers: Array<Int>) :\n    KSerializer<T> {\n    override val descriptor: SerialDescriptor = serialDescriptor<String>()\n\n    init {\n        require(choicesNumbers.size == choices.size){\"There must be exactly one serial number for every enum constant.\"}\n        require(choicesNumbers.distinct().size == choicesNumbers.size){\"There must be no duplicates of serial numbers.\"}\n    }\n\n    final override fun serialize(encoder: Encoder, value: T) {\n        val index = choices.indexOf(value)\n            .also { check(it != -1) { \"$value is not a valid enum $serialName, choices are $choices\" } }\n        encoder.encodeInt(choicesNumbers[index])\n    }\n\n    final override fun deserialize(decoder: Decoder): T {\n        val serialNumber = decoder.decodeInt()\n        val index = choicesNumbers.indexOf(serialNumber)\n        check(index != -1) {\"$serialNumber is not a valid serial value of $serialName, choices are $choicesNumbers\"}\n        check(index in choices.indices)\n        { \"$index is not among valid $serialName choices, choices size is ${choices.size}\" }\n        return choices[index]\n    }\n}\n\ninterface SerialEnum {\n    val serialNumber: Int?\n}\nfun <T> Array<T>.serial() where T : SerialEnum, T : Enum<T> = this.map { it.serialNumber ?: it.ordinal }.toTypedArray()"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/http/util/Zlib.kt",
    "content": "package dev.aaa1115910.biliapi.http.util\n\nimport io.ktor.utils.io.core.use\nimport java.io.ByteArrayOutputStream\nimport java.util.zip.Deflater\nimport java.util.zip.Inflater\n\nfun ByteArray.zlibCompress(): ByteArray {\n    val output = ByteArray(this.size * 4)\n    val compressor = Deflater().apply {\n        setInput(this@zlibCompress)\n        finish()\n    }\n    val compressedDataLength: Int = compressor.deflate(output)\n    return output.copyOfRange(0, compressedDataLength)\n}\n\nfun ByteArray.zlibDecompress(): ByteArray {\n    val inflater = Inflater()\n    try {\n        inflater.setInput(this)\n        // 预估解压后大小（通常是压缩的3-5倍），减少扩容次数\n        val estimatedSize = this.size * 4\n        val outputStream = ByteArrayOutputStream(estimatedSize.coerceAtLeast(1024))\n        return outputStream.use {\n            // 增大缓冲区，减少循环次数\n            val buffer = ByteArray(8192)\n            var count: Int\n            while (inflater.inflate(buffer).also { count = it } > 0) {\n                outputStream.write(buffer, 0, count)\n            }\n            outputStream.toByteArray()\n        }\n    } finally {\n        inflater.end()\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/AuthRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport org.koin.core.annotation.Single\n\n@Single\nclass AuthRepository {\n    var sessionData: String? = null\n    var biliJct: String? = null\n    var accessToken: String? = null\n    var mid: Long? = null\n    var buvid3: String? = null\n    var buvid: String? = null\n    var gaiaVtoken: String? = null\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/BiliApiModule.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport org.koin.core.annotation.ComponentScan\nimport org.koin.core.annotation.Module\n\n@Module\n@ComponentScan\nclass BiliApiModule"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/ChannelRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.grpc.utils.generateChannel\nimport io.grpc.ManagedChannel\nimport org.koin.core.annotation.Single\n\n@Single\nclass ChannelRepository {\n    // grpc.biliapi.net\n    var defaultChannel: ManagedChannel? = null\n\n    // custom proxy server\n    var proxyChannel: ManagedChannel? = null\n\n    fun initDefaultChannel(accessKey: String, buvid: String) {\n        defaultChannel?.shutdownNow()\n        defaultChannel = generateChannel(accessKey, buvid)\n    }\n\n    fun initProxyChannel(accessKey: String, buvid: String, proxyServer: String) {\n        proxyChannel?.shutdownNow()\n        val proxyServerSpilt = proxyServer.split(\":\")\n        val endPoint = proxyServerSpilt.first()\n        val port = proxyServerSpilt.getOrNull(1)?.toInt()\n        proxyChannel = if (port != null) {\n            generateChannel(accessKey, buvid, endPoint, port, port == 443)\n        } else {\n            generateChannel(accessKey, buvid, endPoint)\n        }\n        //proxyChannel = generateChannel(accessKey, buvid, \"192.168.2.125\", 8080, false)\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/CoinRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport org.koin.core.annotation.Single\n\n@Single\nclass CoinRepository(\n    private val authRepository: AuthRepository\n) {\n    suspend fun checkVideoCoin(\n        aid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean {\n        return when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.checkVideoSentCoin(\n                avid = aid,\n                sessData = authRepository.sessionData\n            )\n\n            ApiType.App -> BiliHttpApi.checkVideoSentCoin(\n                avid = aid,\n                accessKey = authRepository.accessToken\n            )\n        }\n    }\n\n    suspend fun addVideoCoin(\n        aid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ) {\n        val (success, message) = when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.sendVideoCoin(\n                avid = aid,\n                like = false,\n                csrf = authRepository.biliJct?: \"\",\n                sessData = authRepository.sessionData!!,\n                buvid3 = authRepository.buvid3!!,\n                accessKey = authRepository.accessToken\n            )\n\n            ApiType.App -> BiliHttpApi.sendVideoCoin(\n                avid = aid,\n                like = false,\n                accessKey = authRepository.accessToken\n            )\n        }\n        if (!success) {\n            throw Exception(\"投币失败: $message\")\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/CommentRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport bilibili.main.community.reply.v1.Mode\nimport bilibili.main.community.reply.v1.ReplyGrpcKt\nimport bilibili.main.community.reply.v1.detailListReq\nimport bilibili.main.community.reply.v1.mainListReq\nimport bilibili.pagination.feedPagination\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.reply.CommentPage\nimport dev.aaa1115910.biliapi.entity.reply.CommentRepliesData\nimport dev.aaa1115910.biliapi.entity.reply.CommentReplyPage\nimport dev.aaa1115910.biliapi.entity.reply.CommentSort\nimport dev.aaa1115910.biliapi.entity.reply.CommentsData\nimport dev.aaa1115910.biliapi.grpc.utils.handleGrpcException\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport kotlinx.serialization.json.Json\nimport org.koin.core.annotation.Single\n\n@Single\nclass CommentRepository(\n    private val authRepository: AuthRepository,\n    private val channelRepository: ChannelRepository\n) {\n    private val replyStub\n        get() = runCatching {\n            ReplyGrpcKt.ReplyCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n\n    suspend fun getComments(\n        id: Long,\n        type: Long,\n        sort: CommentSort = CommentSort.Hot,\n        page: CommentPage = CommentPage(),\n        preferApiType: ApiType = ApiType.Web\n    ): CommentsData {\n        when (preferApiType) {\n            ApiType.Web -> {\n                val webComments = BiliHttpApi.getComments(\n                    oid = id,\n                    type = type,\n                    mode = sort.param,\n                    paginationStr = Json.encodeToString(mapOf(\"offset\" to page.nextWebPage)),\n                    sessData = authRepository.sessionData ?: \"\",\n                    dedeUserID = authRepository.mid,\n                    buvid3 = authRepository.buvid3 ?: \"\"\n                ).getResponseData()\n                return CommentsData.fromCommentData(webComments)\n            }\n\n            ApiType.App -> {\n                runCatching {\n                    val appComments = replyStub?.mainList(\n                        mainListReq {\n                            this.oid = id.toLong()\n                            this.type = type.toLong()\n                            mode = when (sort) {\n                                CommentSort.Hot -> Mode.MAIN_LIST_HOT\n                                CommentSort.HotAndTime -> Mode.DEFAULT\n                                CommentSort.Time -> Mode.MAIN_LIST_TIME\n                            }\n                            pagination = feedPagination {\n                                offset = page.nextAppPage\n                            }\n                        }\n                    ) ?: throw IllegalStateException(\"Reply stub is not initialized\")\n                    return CommentsData.fromMainListReply(appComments)\n                }.onFailure {\n                    handleGrpcException(it)\n                }.getOrThrow()\n            }\n        }\n    }\n\n    suspend fun getCommentReplies(\n        rpid: Long,\n        type: Long,\n        commentId: Long,\n        page: CommentReplyPage = CommentReplyPage(),\n        sort: CommentSort = CommentSort.Hot,\n        preferApiType: ApiType = ApiType.Web\n    ): CommentRepliesData {\n        when (preferApiType) {\n            ApiType.Web -> {\n                val webReplies = BiliHttpApi.getCommentReplies(\n                    oid = commentId,\n                    type = type,\n                    root = rpid,\n                    pageNumber = page.nextWebPage,\n                    sessData = authRepository.sessionData ?: \"\",\n                    dedeUserID = authRepository.mid,\n                    buvid3 = authRepository.buvid3 ?: \"\"\n                ).getResponseData()\n                return CommentRepliesData.fromCommentReplyData(webReplies)\n            }\n\n            ApiType.App -> {\n                val appReplies = replyStub?.detailList(\n                    detailListReq {\n                        this.oid = commentId\n                        this.type = type\n                        root = rpid\n                        mode = when (sort) {\n                            CommentSort.Hot -> Mode.MAIN_LIST_HOT\n                            CommentSort.HotAndTime -> Mode.DEFAULT\n                            CommentSort.Time -> Mode.MAIN_LIST_TIME\n                        }\n                        pagination = feedPagination {\n                            offset = page.nextAppPage\n                        }\n                    }\n                ) ?: throw IllegalStateException(\"Reply stub is not initialized\")\n                return CommentRepliesData.fromCommentReplyList(appReplies)\n            }\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/FavoriteRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.FavoriteFolderData\nimport dev.aaa1115910.biliapi.entity.FavoriteFolderMetadata\nimport dev.aaa1115910.biliapi.entity.FavoriteItemType\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport org.koin.core.annotation.Single\n\n@Single\nclass FavoriteRepository(\n    private val authRepository: AuthRepository\n) {\n    suspend fun checkVideoFavoured(\n        aid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean {\n        return when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.checkVideoFavoured(\n                avid = aid,\n                sessData = authRepository.sessionData\n            )\n\n            ApiType.App -> BiliHttpApi.checkVideoFavoured(\n                avid = aid,\n                accessKey = authRepository.accessToken\n            )\n        }\n    }\n\n    suspend fun addVideoToFavoriteFolder(\n        aid: Long,\n        addMediaIds: List<Long>,\n        preferApiType: ApiType = ApiType.Web\n    ) {\n        when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.setVideoToFavorite(\n                avid = aid,\n                type = FavoriteItemType.Video.value,\n                addMediaIds = addMediaIds,\n                sessData = authRepository.sessionData,\n                csrf = authRepository.biliJct\n            )\n\n            ApiType.App -> BiliHttpApi.setVideoToFavorite(\n                avid = aid,\n                type = FavoriteItemType.Video.value,\n                addMediaIds = addMediaIds,\n                accessKey = authRepository.accessToken\n            )\n        }\n    }\n\n    suspend fun delVideoFromFavoriteFolder(\n        aid: Long,\n        delMediaIds: List<Long>,\n        preferApiType: ApiType = ApiType.Web\n    ) {\n        when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.setVideoToFavorite(\n                avid = aid,\n                type = FavoriteItemType.Video.value,\n                delMediaIds = delMediaIds,\n                sessData = authRepository.sessionData,\n                csrf = authRepository.biliJct\n            )\n\n            ApiType.App -> BiliHttpApi.setVideoToFavorite(\n                avid = aid,\n                type = FavoriteItemType.Video.value,\n                delMediaIds = delMediaIds,\n                accessKey = authRepository.accessToken\n            )\n        }\n    }\n\n    suspend fun updateVideoToFavoriteFolder(\n        aid: Long,\n        addMediaIds: List<Long>,\n        delMediaIds: List<Long>,\n        preferApiType: ApiType = ApiType.Web\n    ) {\n        when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.setVideoToFavorite(\n                avid = aid,\n                type = FavoriteItemType.Video.value,\n                addMediaIds = addMediaIds,\n                delMediaIds = delMediaIds,\n                sessData = authRepository.sessionData,\n                csrf = authRepository.biliJct\n            )\n\n            ApiType.App -> BiliHttpApi.setVideoToFavorite(\n                avid = aid,\n                type = FavoriteItemType.Video.value,\n                addMediaIds = addMediaIds,\n                delMediaIds = delMediaIds,\n                accessKey = authRepository.accessToken\n            )\n        }\n    }\n\n    suspend fun getAllFavoriteFolderMetadataList(\n        mid: Long,\n        type: FavoriteItemType = FavoriteItemType.Video,\n        rid: Long? = null,\n        preferApiType: ApiType = ApiType.Web\n    ): List<FavoriteFolderMetadata> {\n        val userFavoriteFoldersData = when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.getAllFavoriteFoldersInfo(\n                mid = mid,\n                type = type.value,\n                rid = rid,\n                sessData = authRepository.sessionData ?: \"\"\n            )\n\n            ApiType.App -> BiliHttpApi.getAllFavoriteFoldersInfo(\n                mid = mid,\n                type = type.value,\n                rid = rid,\n                accessKey = authRepository.accessToken ?: \"\"\n            )\n        }.getResponseData()\n        return userFavoriteFoldersData.list.map {\n            FavoriteFolderMetadata.fromHttpUserFavoriteFolder(it)\n        }\n    }\n\n    suspend fun getFavoriteFolderData(\n        mediaId: Long,\n        pageSize: Int = 20,\n        pageNumber: Int = 1,\n        preferApiType: ApiType = ApiType.Web\n    ): FavoriteFolderData {\n        val favoriteFolderListData = when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.getFavoriteList(\n                mediaId = mediaId,\n                pageSize = pageSize,\n                pageNumber = pageNumber,\n                sessData = authRepository.sessionData ?: \"\"\n            )\n\n            ApiType.App -> BiliHttpApi.getFavoriteList(\n                mediaId = mediaId,\n                pageSize = pageSize,\n                pageNumber = pageNumber,\n                accessKey = authRepository.accessToken ?: \"\"\n            )\n        }.getResponseData()\n        return FavoriteFolderData.fromHttpFavoriteFolderInfoListData(favoriteFolderListData)\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/HistoryRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport bilibili.app.interfaces.v1.HistoryGrpcKt\nimport bilibili.app.interfaces.v1.cursor\nimport bilibili.app.interfaces.v1.cursorV2Req\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.user.HistoryData\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport org.koin.core.annotation.Single\n\n@Single\nclass HistoryRepository(\n    private val authRepository: AuthRepository,\n    private val channelRepository: ChannelRepository\n) {\n    private val historyStub\n        get() = runCatching {\n            HistoryGrpcKt.HistoryCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n\n    suspend fun getHistories(\n        cursor: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): HistoryData {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val data = BiliHttpApi.getHistories(\n                    viewAt = cursor,\n                    sessData = authRepository.sessionData!!,\n                ).getResponseData()\n                HistoryData.fromHistoryResponse(data)\n            }\n\n            ApiType.App -> {\n                val reply = historyStub?.cursorV2(cursorV2Req {\n                    this.cursor = cursor {\n                        max = cursor\n                    }\n                    business = \"archive\"\n                })\n                HistoryData.fromHistoryResponse(reply!!)\n            }\n        }\n    }\n\n    suspend fun deleteHistory(\n        business: String,\n        kid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean {\n        return runCatching {\n            BiliHttpApi.deleteHistory(\n                kid = \"${business}_$kid\",\n                csrf = authRepository.biliJct!!,\n                sessData = authRepository.sessionData!!\n            ).code == 0\n        }.getOrDefault(false)\n    }\n\n    suspend fun clearHistory(\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean {\n        return runCatching {\n            when (preferApiType) {\n                ApiType.Web, ApiType.App -> {\n                    BiliHttpApi.clearHistory(\n                        csrf = authRepository.biliJct!!,\n                        sessData = authRepository.sessionData!!\n                    ).code == 0\n                }\n            }\n        }.getOrDefault(false)\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/LikeRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport org.koin.core.annotation.Single\n\n@Single\nclass LikeRepository(\n    private val authRepository: AuthRepository\n) {\n    suspend fun checkVideoLike(\n        aid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean {\n        return when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.checkVideoLiked(\n                avid = aid,\n                sessData = authRepository.sessionData\n            )\n\n            ApiType.App -> BiliHttpApi.checkVideoLiked(\n                avid = aid,\n                accessKey = authRepository.accessToken\n            )\n        }\n    }\n\n    suspend fun addVideoLike(\n        aid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ) {\n        val (success, message) = when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.sendVideoLike(\n                avid = aid,\n                like = true,\n                sessData = authRepository.sessionData,\n                csrf = authRepository.biliJct\n            )\n\n            ApiType.App -> BiliHttpApi.sendVideoLike(\n                avid = aid,\n                like = true,\n                accessKey = authRepository.accessToken\n            )\n        }\n        if (!success) {\n            throw Exception(\"点赞失败: $message\")\n        }\n    }\n\n    suspend fun delVideoLike(\n        aid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ) {\n        val (success, message) = when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.sendVideoLike(\n                avid = aid,\n                like = false,\n                sessData = authRepository.sessionData,\n                csrf = authRepository.biliJct\n            )\n\n            ApiType.App -> BiliHttpApi.sendVideoLike(\n                avid = aid,\n                like = false,\n                accessKey = authRepository.accessToken\n            )\n        }\n        if (!success) {\n            throw Exception(\"取消点赞失败: $message\")\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/LiveRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.live.LiveAreaResponse\nimport dev.aaa1115910.biliapi.entity.live.LiveFollowingResponse\nimport dev.aaa1115910.biliapi.entity.live.LiveRecommendResponse\nimport dev.aaa1115910.biliapi.entity.live.LiveRoomListResponse\nimport dev.aaa1115910.biliapi.entity.live.LiveRoomPlayInfoResponse\nimport dev.aaa1115910.biliapi.http.plugins.BiliUserAgent\nimport dev.aaa1115910.biliapi.http.util.BiliDns\nimport dev.aaa1115910.biliapi.http.util.encAppGet\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.json.Json\nimport org.koin.core.annotation.Single\n\n@Single\nclass LiveRepository(\n    private val authRepository: AuthRepository\n) {\n    /** 暴露登录状态供 ViewModel 判断 */\n    val sessionData: String? get() = authRepository.sessionData\n\n    private val client = HttpClient(OkHttp) {\n        engine {\n            config {\n                dns(BiliDns)\n            }\n        }\n        BiliUserAgent()\n        install(ContentNegotiation) {\n            json(Json {\n                coerceInputValues = true\n                ignoreUnknownKeys = true\n                prettyPrint = true\n            })\n        }\n    }\n\n    /**\n     * 获取直播分区列表\n     */\n    suspend fun getLiveAreaList(): LiveAreaResponse = withContext(Dispatchers.IO) {\n        client.get(\"https://api.live.bilibili.com/room/v1/Area/getList\")\n            .body()\n    }\n\n    /**\n     * 获取直播间列表\n     * @param parentAreaId 父分区ID\n     * @param areaId 分区ID\n     * @param page 页码\n     * @param pageSize 每页数量\n     */\n    suspend fun getLiveRoomList(\n        parentAreaId: String,\n        areaId: String = \"0\",\n        page: Int = 1,\n        pageSize: Int = 30,\n    ): LiveRoomListResponse = withContext(Dispatchers.IO) {\n        client.get(\"https://api.live.bilibili.com/xlive/app-interface/v2/second/getList\") {\n            parameter(\"access_key\", authRepository.accessToken ?: \"\")\n            parameter(\"actionKey\", \"appkey\")\n            parameter(\"area_id\", areaId)\n            parameter(\"build\", 8430300)\n            parameter(\"device\", \"android\")\n            parameter(\"mobi_app\", \"android\")\n            parameter(\"page\", page)\n            parameter(\"page_size\", pageSize)\n            parameter(\"parent_area_id\", parentAreaId)\n            parameter(\"platform\", \"android\")\n            parameter(\"qn\", 0)\n            parameter(\"sort_type\", \"\")\n            encAppGet()\n            header(\"buvid\", authRepository.buvid ?: \"\")\n            header(\"env\", \"prod\")\n            header(\"app-key\", \"android\")\n            header(\"x-bili-trace-id\", \"\")\n            header(\"x-bili-aurora-eid\", \"\")\n            header(\"x-bili-aurora-zone\", \"\")\n        }.body()\n    }\n\n    /**\n     * 获取推荐直播列表\n     * @param page 页码\n     */\n    suspend fun getLiveRecommendList(\n        page: Int = 1,\n    ): LiveRecommendResponse = withContext(Dispatchers.IO) {\n        client.get(\"https://api.live.bilibili.com/xlive/web-interface/v1/webMain/getMoreRecList\") {\n            parameter(\"platform\", \"web\")\n            // parameter(\"page\", page)\n        }.body()\n    }\n\n    /**\n     * 获取关注的正在直播列表\n     * @param page 页码\n     * @param pageSize 每页数量（1~30）\n     */\n    suspend fun getLiveFollowingList(\n        page: Int = 1,\n        pageSize: Int = 10,\n    ): LiveFollowingResponse = withContext(Dispatchers.IO) {\n        client.get(\"https://api.live.bilibili.com/xlive/web-ucenter/user/following\") {\n            parameter(\"page\", page)\n            parameter(\"page_size\", pageSize)\n            parameter(\"ignoreRecord\", \"1\")\n            parameter(\"hit_ab\", \"true\")\n            val sessData = authRepository.sessionData ?: \"\"\n            if (sessData.isNotEmpty()) {\n                header(\"Cookie\", \"SESSDATA=$sessData\")\n            }\n        }.body()\n    }\n\n    /**\n     * 获取直播间播放信息\n     * @param roomId 直播间ID\n     * @param qn 画质编号，默认30000（杜比，最高），服务端会自动降级到实际最高可用画质\n     * @param sessData 登录凭证，需要认证才能获取高画质流\n     */\n    suspend fun getLiveRoomPlayInfo(roomId: Int, qn: Int = 30000, sessData: String = \"\"): LiveRoomPlayInfoResponse =\n        withContext(Dispatchers.IO) {\n            client.get(\"https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo\") {\n                parameter(\"room_id\", roomId)\n                parameter(\"qn\", qn)\n                parameter(\"platform\", \"web\")\n                parameter(\"protocol\", \"0,1\")\n                parameter(\"format\", \"0,1,2\")\n                parameter(\"codec\", \"0,1,2\")\n                parameter(\"dolby\", 5)\n                parameter(\"panorama\", 1)\n                if (sessData.isNotEmpty()) {\n                    header(\"Cookie\", \"SESSDATA=$sessData\")\n                }\n            }.body()\n        }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/LoginRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.login.Captcha\nimport dev.aaa1115910.biliapi.entity.login.QrLoginData\nimport dev.aaa1115910.biliapi.entity.login.QrLoginResult\nimport dev.aaa1115910.biliapi.entity.login.QrLoginState\nimport dev.aaa1115910.biliapi.entity.login.SmsLoginResult\nimport dev.aaa1115910.biliapi.entity.login.WebCookies\nimport dev.aaa1115910.biliapi.http.BiliPassportHttpApi\nimport io.ktor.util.date.toJvmDate\nimport org.koin.core.annotation.Single\nimport java.util.Date\nimport java.util.UUID\n\n@Single\nclass LoginRepository {\n    /**\n     * 请求扫码登录的二维码，仅支持 Http 接口使用\n     */\n    suspend fun requestWebQrLogin(): QrLoginData {\n        val response = BiliPassportHttpApi.getWebQRUrl().getResponseData()\n        return QrLoginData(\n            url = response.url,\n            key = response.qrcodeKey\n        )\n    }\n\n    /**\n     * 检查扫码登录情况\n     *\n     * @param qrcodeKey 二维码内容\n     */\n    suspend fun checkWebQrLoginState(qrcodeKey: String): QrLoginResult {\n        val (response, cookies) = BiliPassportHttpApi.loginWithWebQR(qrcodeKey)\n        val responseData = response.getResponseData()\n        var resultCookies: WebCookies? = null\n        val resultState = when (responseData.code) {\n            0 -> {\n                resultCookies = WebCookies(\n                    dedeUserId = cookies.find { it.name == \"DedeUserID\" }?.value?.toLong()\n                        ?: throw IllegalArgumentException(\"Cookie DedeUserID not found\"),\n                    dedeUserIdCkMd5 = cookies.find { it.name == \"DedeUserID__ckMd5\" }?.value\n                        ?: throw IllegalArgumentException(\"Cookie DedeUserID__ckMd5 not found\"),\n                    sid = cookies.find { it.name == \"sid\" }?.value\n                        ?: throw IllegalArgumentException(\"Cookie sid not found\"),\n                    biliJct = cookies.find { it.name == \"bili_jct\" }?.value\n                        ?: throw IllegalArgumentException(\"Cookie bili_jct not found\"),\n                    sessData = cookies.find { it.name == \"SESSDATA\" }?.value\n                        ?: throw IllegalArgumentException(\"Cookie SESSDATA not found\"),\n                    expiredDate = cookies.firstOrNull()?.expires?.toJvmDate()\n                        ?: throw IllegalArgumentException(\"Cookie expires date not found\")\n                )\n                QrLoginState.Success\n            }\n\n            86101 -> QrLoginState.WaitingForScan\n            86090 -> QrLoginState.WaitingForConfirm\n            86038 -> QrLoginState.Expired\n            else -> QrLoginState.Unknown\n        }\n        return QrLoginResult(\n            state = resultState,\n            accessToken = null,\n            refreshToken = null,\n            cookies = resultCookies\n        )\n    }\n\n    /**\n     * 请求扫码登录的二维码，支持 Http+gRPC 接口使用\n     */\n    suspend fun requestAppQrLogin(): QrLoginData {\n        val response = BiliPassportHttpApi.getAppQRUrl(\n            localId = \"0\",\n            ts = (System.currentTimeMillis() / 1000).toInt(),\n            mobiApp = \"android_hd\"\n        ).getResponseData()\n        return QrLoginData(\n            url = response.url,\n            key = response.authCode\n        )\n    }\n\n    /**\n     * 检查扫码登录情况\n     *\n     * @param authCode 二维码内容\n     */\n    suspend fun checkAppQrLoginState(authCode: String): QrLoginResult {\n        val response = BiliPassportHttpApi.loginWithAppQR(\n            authCode = authCode,\n            localId = \"0\",\n            ts = (System.currentTimeMillis() / 1000).toInt(),\n        )\n        println(response)\n        var resultCookies: WebCookies? = null\n        val resultState = when (response.code) {\n            0 -> {\n                resultCookies = WebCookies(\n                    dedeUserId = response.getResponseData().cookieInfo.cookies.find { it.name == \"DedeUserID\" }?.value?.toLong()\n                        ?: throw IllegalArgumentException(\"Cookie DedeUserID not found\"),\n                    dedeUserIdCkMd5 = response.getResponseData().cookieInfo.cookies.find { it.name == \"DedeUserID__ckMd5\" }?.value\n                        ?: throw IllegalArgumentException(\"Cookie DedeUserID__ckMd5 not found\"),\n                    sid = response.getResponseData().cookieInfo.cookies.find { it.name == \"sid\" }?.value\n                        ?: throw IllegalArgumentException(\"Cookie sid not found\"),\n                    biliJct = response.getResponseData().cookieInfo.cookies.find { it.name == \"bili_jct\" }?.value\n                        ?: throw IllegalArgumentException(\"Cookie bili_jct not found\"),\n                    sessData = response.getResponseData().cookieInfo.cookies.find { it.name == \"SESSDATA\" }?.value\n                        ?: throw IllegalArgumentException(\"Cookie SESSDATA not found\"),\n                    expiredDate = Date(response.getResponseData().cookieInfo.cookies.firstOrNull()?.expires?.times(1000L)\n                        ?: throw IllegalArgumentException(\"Cookie expires date not found\"))\n                )\n                QrLoginState.Success\n            }\n\n            86039 -> QrLoginState.WaitingForScan\n            86090 -> QrLoginState.WaitingForConfirm\n            86038 -> QrLoginState.Expired\n            else -> QrLoginState.Unknown\n        }\n        return QrLoginResult(\n            state = resultState,\n            accessToken = response.data?.accessToken,\n            refreshToken = response.data?.refreshToken,\n            cookies = resultCookies\n        )\n    }\n\n    /**\n     * 申请 captcha 验证码\n     */\n    suspend fun getCaptcha(): Captcha {\n        val captchaData = BiliPassportHttpApi.getCaptcha().getResponseData()\n        return Captcha(\n            token = captchaData.token,\n            challenge = captchaData.geetest.challenge,\n            gt = captchaData.geetest.gt\n        )\n    }\n\n    fun generateLoginSessionId() = UUID.randomUUID().toString().replace(\"-\", \"\")\n\n    /**\n     * 请求验证码\n     */\n    suspend fun requestSms(\n        phone: Long,\n        loginSessionId: String,\n        buvid: String,\n        recaptchaToken: String? = null,\n        geetestChallenge: String? = null,\n        geetestValidate: String? = null\n    ): SendSmsResult {\n        val response = BiliPassportHttpApi.sendSms(\n            cid = 86,\n            tel = phone,\n            loginSessionId = loginSessionId,\n            recaptchaToken = recaptchaToken,\n            geeChallenge = geetestChallenge,\n            geeValidate = geetestValidate,\n            geeSeccode = \"$geetestValidate|jordan\",\n            channel = \"bili\",\n            buvid = buvid,\n            statistics = \"\"\"{\"appId\":1,\"platform\":3,\"version\":\"7.27.0\",\"abtest\":\"\"}\"\"\",\n            ts = System.currentTimeMillis() / 1000\n        )\n        return if (response.code == 0 && response.data != null) {\n            if (response.data.captchaKey != \"\") {\n                SendSmsResult(\n                    state = SendSmsState.Success,\n                    captchaKey = response.data.captchaKey\n                )\n            } else {\n                SendSmsResult(\n                    state = SendSmsState.RecaptchaRequire,\n                    recaptchaUrl = response.data.recaptchaUrl\n                )\n            }\n        } else {\n            SendSmsResult(\n                state = SendSmsState.Error,\n                message = response.message\n            )\n        }\n    }\n\n    /**\n     * 验证码登录\n     */\n    suspend fun loginWithSms(\n        phone: Long,\n        loginSessionId: String,\n        code: Int,\n        captchaKey: String\n    ): SmsLoginResult {\n        val response = BiliPassportHttpApi.loginWithSms(\n            cid = 86,\n            tel = phone,\n            loginSessionId = loginSessionId,\n            code = code,\n            captchaKey = captchaKey\n        ).getResponseData()\n        return SmsLoginResult.fromSmsLoginResponse(response)\n    }\n\n    suspend fun getbuvid3 () : String {\n        return BiliPassportHttpApi.getbuvid3()\n    }\n}\n\ndata class SendSmsResult(\n    val state: SendSmsState,\n    val message: String = \"\",\n    val captchaKey: String? = null,\n    val recaptchaUrl: String? = null\n)\n\nenum class SendSmsState {\n    Ready,\n    Error,\n    Success,\n    RecaptchaRequire\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/PgcRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.CarouselData\nimport dev.aaa1115910.biliapi.entity.pgc.PgcFeedData\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.entity.pgc.index.Area\nimport dev.aaa1115910.biliapi.entity.pgc.index.Copyright\nimport dev.aaa1115910.biliapi.entity.pgc.index.IndexOrder\nimport dev.aaa1115910.biliapi.entity.pgc.index.IndexOrderType\nimport dev.aaa1115910.biliapi.entity.pgc.index.IsFinish\nimport dev.aaa1115910.biliapi.entity.pgc.index.PgcIndexConditionData\nimport dev.aaa1115910.biliapi.entity.pgc.index.PgcIndexData\nimport dev.aaa1115910.biliapi.entity.pgc.index.Producer\nimport dev.aaa1115910.biliapi.entity.pgc.index.ReleaseDate\nimport dev.aaa1115910.biliapi.entity.pgc.index.SeasonMonth\nimport dev.aaa1115910.biliapi.entity.pgc.index.SeasonStatus\nimport dev.aaa1115910.biliapi.entity.pgc.index.SeasonVersion\nimport dev.aaa1115910.biliapi.entity.pgc.index.SpokenLanguage\nimport dev.aaa1115910.biliapi.entity.pgc.index.Style\nimport dev.aaa1115910.biliapi.entity.pgc.index.Year\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.biliapi.http.SeasonIndexType\nimport org.koin.core.annotation.Single\n\n@Single\nclass PgcRepository {\n    suspend fun getCarousel(pgcType: PgcType): CarouselData {\n        val initialStateData = BiliHttpApi.getPgcWebInitialStateData(pgcType)\n        val carouselData = CarouselData.fromPgcWebInitialStateData(initialStateData)\n        return carouselData\n    }\n\n    suspend fun getFeed(pgcType: PgcType, cursor: Int): PgcFeedData {\n        val data = when (pgcType) {\n            PgcType.Anime, PgcType.GuoChuang -> PgcFeedData.fromPgcFeedData(\n                BiliHttpApi.getPgcFeedV3(\n                    name = pgcType.name.lowercase(),\n                    cursor = cursor\n                ).getResponseData()\n            )\n\n            PgcType.Movie, PgcType.Tv, PgcType.Documentary, PgcType.Variety -> PgcFeedData.fromPgcFeedData(\n                BiliHttpApi.getPgcFeed(\n                    name = pgcType.name.lowercase(),\n                    cursor = cursor\n                ).getResponseData()\n            )\n        }\n        return data\n    }\n\n    suspend fun getPgcIndexCondition(pgcType: PgcType): PgcIndexConditionData {\n        return BiliHttpApi.seasonIndexCondition(\n            seasonIndexType = pgcType.toSeasonIndexType(),\n            type = 0\n        ).getResponseData()\n    }\n\n    suspend fun getPgcIndex(\n        pgcType: PgcType,\n        order: String,\n        sort: String,\n        filters: Map<String, String>,\n        page: PgcIndexData.PgcIndexPage,\n    ): PgcIndexData {\n        val data = PgcIndexData.fromIndexResultData(\n            BiliHttpApi.seasonIndexDynamicResult(\n                seasonIndexType = pgcType.toSeasonIndexType(),\n                order = order,\n                sort = sort,\n                filters = filters,\n                page = page.nextPage,\n                pagesize = page.pageSize,\n                type = 0\n            ).getResponseData()\n        )\n        return data\n    }\n\n    suspend fun getPgcIndex(\n        pgcType: PgcType,\n        indexOrder: IndexOrder,\n        indexOrderType: IndexOrderType,\n        seasonVersion: SeasonVersion,\n        spokenLanguage: SpokenLanguage,\n        area: Area,\n        isFinish: IsFinish,\n        copyright: Copyright,\n        seasonStatus: SeasonStatus,\n        seasonMonth: SeasonMonth,\n        producer: Producer,\n        year: Year,\n        releaseDate: ReleaseDate,\n        style: Style,\n        page: PgcIndexData.PgcIndexPage,\n    ): PgcIndexData {\n        val data = PgcIndexData.fromIndexResultData(\n            when (pgcType) {\n                PgcType.Anime -> BiliHttpApi.seasonIndexAnimeResult(\n                    order = indexOrder.id,\n                    sort = indexOrderType.id,\n                    seasonVersion = seasonVersion.id,\n                    spokenLanguageType = spokenLanguage.id,\n                    area = area.id,\n                    isFinish = isFinish.id,\n                    copyright = copyright.id,\n                    seasonStatus = seasonStatus.id,\n                    seasonMonth = seasonMonth.id,\n                    year = year.str,\n                    styleId = style.id,\n                    page = page.nextPage,\n                    pagesize = page.pageSize\n                )\n\n                PgcType.GuoChuang -> BiliHttpApi.seasonIndexGuochuangResult(\n                    order = indexOrder.id,\n                    sort = indexOrderType.id,\n                    seasonVersion = seasonVersion.id,\n                    isFinish = isFinish.id,\n                    copyright = copyright.id,\n                    seasonStatus = seasonStatus.id,\n                    year = year.str,\n                    styleId = style.id,\n                    page = page.nextPage,\n                    pagesize = page.pageSize\n                )\n\n                PgcType.Movie -> BiliHttpApi.seasonIndexMovieResult(\n                    order = indexOrder.id,\n                    sort = indexOrderType.id,\n                    area = area.id,\n                    seasonStatus = seasonStatus.id,\n                    releaseDate = releaseDate.str,\n                    styleId = style.id,\n                    page = page.nextPage,\n                    pagesize = page.pageSize\n                )\n\n                PgcType.Documentary -> BiliHttpApi.seasonIndexDocumentaryResult(\n                    order = indexOrder.id,\n                    sort = indexOrderType.id,\n                    area = area.id,\n                    seasonStatus = seasonStatus.id,\n                    producerId = producer.id,\n                    releaseDate = releaseDate.str,\n                    styleId = style.id,\n                    page = page.nextPage,\n                    pagesize = page.pageSize\n                )\n\n                PgcType.Tv -> BiliHttpApi.seasonIndexTvResult(\n                    order = indexOrder.id,\n                    sort = indexOrderType.id,\n                    area = area.id,\n                    seasonStatus = seasonStatus.id,\n                    releaseDate = releaseDate.str,\n                    styleId = style.id,\n                    page = page.nextPage,\n                    pagesize = page.pageSize\n                )\n\n                PgcType.Variety -> BiliHttpApi.seasonIndexVarietyResult(\n                    order = indexOrder.id,\n                    sort = indexOrderType.id,\n                    seasonStatus = seasonStatus.id,\n                    styleId = style.id,\n                    page = page.nextPage,\n                    pagesize = page.pageSize\n                )\n            }.getResponseData()\n        )\n        return data\n    }\n}\n\nprivate fun PgcType.toSeasonIndexType() = when (this) {\n    PgcType.Anime -> SeasonIndexType.Anime\n    PgcType.GuoChuang -> SeasonIndexType.Guochuang\n    PgcType.Movie -> SeasonIndexType.Movie\n    PgcType.Documentary -> SeasonIndexType.Documentary\n    PgcType.Tv -> SeasonIndexType.Tv\n    PgcType.Variety -> SeasonIndexType.Variety\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/RecommendVideoRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport bilibili.app.show.v1.PopularGrpcKt\nimport bilibili.app.show.v1.popularResultReq\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.home.RecommendData\nimport dev.aaa1115910.biliapi.entity.home.RecommendPage\nimport dev.aaa1115910.biliapi.entity.rank.PopularVideoData\nimport dev.aaa1115910.biliapi.entity.rank.PopularVideoPage\nimport dev.aaa1115910.biliapi.entity.ugc.UgcItem\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport org.koin.core.annotation.Single\n\n@Single\nclass RecommendVideoRepository(\n    private val authRepository: AuthRepository,\n    private val channelRepository: ChannelRepository\n) {\n    private val popularStub\n        get() = runCatching {\n            PopularGrpcKt.PopularCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n\n    suspend fun getPopularVideos(\n        page: PopularVideoPage,\n        preferApiType: ApiType = ApiType.Web\n    ): PopularVideoData {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val response = BiliHttpApi.getPopularVideoData(\n                    pageSize = page.nextWebPageSize,\n                    pageNumber = page.nextWebPageNumber,\n                    sessData = authRepository.sessionData ?: \"\"\n                ).getResponseData()\n                val list = response.list.map { UgcItem.fromVideoInfo(it) }\n                val nextPage = PopularVideoPage(\n                    nextWebPageSize = page.nextWebPageSize,\n                    nextWebPageNumber = page.nextWebPageNumber + 1\n                )\n                PopularVideoData(\n                    list = list,\n                    nextPage = nextPage,\n                    noMore = response.noMore\n                )\n            }\n\n            ApiType.App -> {\n                val reply = popularStub?.index(popularResultReq {\n                    idx = page.nextAppIndex.toLong()\n                })\n                val list = reply?.itemsList\n                    ?.filter { it.itemCase == bilibili.app.card.v1.Card.ItemCase.SMALL_COVER_V5 }\n                    ?.map { UgcItem.fromSmallCoverV5(it.smallCoverV5) }\n                    ?: emptyList()\n                val nextPage = PopularVideoPage(\n                    nextAppIndex = list.lastOrNull()?.idx ?: -1\n                )\n                PopularVideoData(\n                    list = list,\n                    nextPage = nextPage,\n                    noMore = nextPage.nextAppIndex == -1\n                )\n            }\n        }\n    }\n\n    suspend fun getRecommendVideos(\n        page: RecommendPage = RecommendPage(),\n        preferApiType: ApiType = ApiType.Web\n    ): RecommendData {\n        val items = when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.getFeedRcmd(\n                idx = page.nextWebIdx,\n                sessData = authRepository.sessionData\n            )\n                .getResponseData().item\n                .map { UgcItem.fromRcmdItem(it) }\n\n            ApiType.App -> BiliHttpApi.getFeedIndex(\n                idx = page.nextAppIdx,\n                accessKey = authRepository.accessToken\n            )\n                .getResponseData().items\n                .filter { it.cardGoto == \"av\" }\n                .map { UgcItem.fromRcmdItem(it) }\n        }\n        val nextPage = when (preferApiType) {\n            ApiType.Web -> RecommendPage(\n                nextWebIdx = page.nextWebIdx + 1\n            )\n\n            ApiType.App -> RecommendPage(\n                nextAppIdx = items.first().idx + 1\n            )\n        }\n        return RecommendData(\n            items = items,\n            nextPage = nextPage\n        )\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/SearchRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport bilibili.app.interfaces.v1.suggestionResult3Req\nimport bilibili.pagination.pagination\nimport bilibili.polymer.app.search.v1.SearchByTypeRequest\nimport bilibili.polymer.app.search.v1.searchByTypeRequest\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.search.Hotword\nimport dev.aaa1115910.biliapi.grpc.utils.handleGrpcException\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.biliapi.http.BiliHttpProxyApi\nimport org.koin.core.annotation.Single\n\n@Single\nclass SearchRepository(\n    private val authRepository: AuthRepository,\n    private val channelRepository: ChannelRepository\n) {\n    private val searchSuggestStub\n        get() = runCatching {\n            bilibili.app.interfaces.v1.SearchGrpcKt.SearchCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n\n    private val searchResultStub\n        get() = runCatching {\n            bilibili.polymer.app.search.v1.SearchGrpcKt.SearchCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n\n    private val proxySearchResultStub\n        get() = runCatching {\n            bilibili.polymer.app.search.v1.SearchGrpcKt.SearchCoroutineStub(channelRepository.proxyChannel!!)\n        }.getOrNull()\n\n    /*private val searchStub\n        get() = runCatching {\n            SearchGrpcKt.SearchCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n\n    suspend fun search(\n        keyword: String,\n        page: Int = 1,\n        pageSize: Int = 20,\n        preferApiType: ApiType = ApiType.Web\n    ): SearchData {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val data = BiliHttpApi.search(\n                    keyword = keyword,\n                    page = page,\n                    pageSize = pageSize,\n                    sessData = authRepository.sessionData!!,\n                ).getResponseData()\n                SearchData.fromSearchResponse(data)\n            }\n\n            ApiType.App -> {\n                val reply = searchStub?.searchV2(searchV2Req {\n                    this.keyword = keyword\n                    this.page = page\n                    this.pageSize = pageSize\n                })\n                SearchData.fromSearchResponse(reply!!)\n            }\n        }\n    }*/\n\n    suspend fun getSearchHotwords(\n        limit: Int = 30,\n        preferApiType: ApiType = ApiType.Web\n    ): List<Hotword> {\n        return when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.getWebSearchSquare(limit = limit)\n                .getResponseData().trending.list\n                .map { Hotword.fromHttpWebHotword(it) }\n\n            /*ApiType.App -> BiliHttpApi.getAppSearchSquare(limit = limit)\n                .getResponseData()\n                .firstOrNull { it.type == \"trending\" }\n                ?.data?.list\n                ?.map { Hotword.fromHttpAppSquareDataItem(it) }\n                ?: emptyList()*/\n\n            ApiType.App -> BiliHttpApi.getSearchTrendRank(limit = 50)\n                .getResponseData().list\n                .map { Hotword.fromHttpAppSearchTrendingHotword(it) }\n        }\n    }\n\n    suspend fun getSearchSuggest(\n        keyword: String,\n        preferApiType: ApiType = ApiType.App\n    ): List<String> {\n        return when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.getKeywordSuggest(\n                term = keyword,\n                buvid = authRepository.buvid ?: \"\",\n            ).suggests.map { it.value }\n\n            //TODO 返回的关键词提示中可能包含通过avid/bvid/专栏id等的直达跳转结果项，需要过滤掉或进行单独处理\n            ApiType.App -> searchSuggestStub?.suggest3(suggestionResult3Req {\n                this.keyword = keyword\n            })?.listList?.map { it.keyword } ?: emptyList()\n        }\n    }\n\n    /**\n     * 按分类进行搜索\n     *\n     * app 端的接口无法对视频投稿结果进行筛选搜索\n     */\n    suspend fun searchType(\n        keyword: String,\n        type: SearchType,\n        tid: Int?,\n        order: SearchFilterOrderType,\n        duration: SearchFilterDuration,\n        page: SearchTypePage,\n        preferApiType: ApiType = ApiType.App,\n        enableProxy: Boolean = false\n    ): SearchTypeResult {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val response = if (enableProxy) {\n                    BiliHttpProxyApi.searchType(\n                        keyword = keyword,\n                        type = type.httpTypeParam,\n                        page = page.nextPageForWeb,\n                        tid = tid,\n                        order = order.httpOrderParam,\n                        duration = duration.httpDurationParam,\n                        sessData = authRepository.sessionData,\n                        buvid3 = authRepository.buvid3,\n                    )\n                } else {\n                    BiliHttpApi.searchType(\n                        keyword = keyword,\n                        type = type.httpTypeParam,\n                        page = page.nextPageForWeb,\n                        tid = tid,\n                        order = order.httpOrderParam,\n                        duration = duration.httpDurationParam,\n                        sessData = authRepository.sessionData,\n                        buvid3 = authRepository.buvid3,\n                    )\n                }.getResponseData()\n                SearchTypeResult.fromSearchTypeResult(response)\n            }\n\n            ApiType.App -> {\n                val searchTypeReply = runCatching {\n                    val searchTypeRequest = searchByTypeRequest {\n                        this.keyword = keyword\n                        this.type = type.grpcTypeParam\n                        categorySort = order.grpcOrderParam\n                        userType = SearchByTypeRequest.UserType.ALL\n                        userSort = SearchByTypeRequest.UserSort.USER_SORT_DEFAULT\n                        pagination = pagination {\n                            next = page.nextPageForApp\n                        }\n                    }\n                    if (enableProxy) {\n                        proxySearchResultStub?.searchByType(searchTypeRequest)\n                            ?: throw IllegalStateException(\"Proxy search result stub is not initialized\")\n                    } else {\n                        searchResultStub?.searchByType(searchTypeRequest)\n                            ?: throw IllegalStateException(\"Search result stub is not initialized\")\n                    }\n                }.onFailure { handleGrpcException(it) }.getOrThrow()\n                SearchTypeResult.fromSearchTypeResult(searchTypeReply)\n            }\n        }\n    }\n}\n\ndata class SearchTypePage(\n    val nextPageForWeb: Int = 1,\n    val nextPageForApp: String = \"\"\n)\n\nenum class SearchType(\n    val httpTypeParam: String,\n    val grpcTypeParam: Int\n) {\n    Video(httpTypeParam = \"video\", grpcTypeParam = 10),\n    MediaBangumi(httpTypeParam = \"media_bangumi\", grpcTypeParam = 7),\n    MediaFt(httpTypeParam = \"media_ft\", grpcTypeParam = 8),\n    BiliUser(httpTypeParam = \"bili_user\", grpcTypeParam = 2),\n    LiveRoom(httpTypeParam = \"live_room\", grpcTypeParam = 4)\n    //Live grpcTypeParam = 4/5\n    //Article grpcTypeParam = 6\n}\n\nenum class SearchFilterOrderType(\n    val httpOrderParam: String?,\n    val grpcOrderParam: SearchByTypeRequest.CategorySort\n) {\n    ComprehensiveSort(\n        httpOrderParam = null,\n        grpcOrderParam = SearchByTypeRequest.CategorySort.CATEGORY_SORT_DEFAULT\n    ),\n    MostClicks(\n        httpOrderParam = \"click\",\n        grpcOrderParam = SearchByTypeRequest.CategorySort.CATEGORY_SORT_CLICK_COUNT\n    ),\n    LatestPublish(\n        httpOrderParam = \"pubdate\",\n        grpcOrderParam = SearchByTypeRequest.CategorySort.CATEGORY_SORT_PUBLISH_TIME\n    ),\n    MostDanmaku(\n        httpOrderParam = \"dm\",\n        grpcOrderParam = SearchByTypeRequest.CategorySort.UNRECOGNIZED\n    ),\n    MostFavorites(\n        httpOrderParam = \"stow\",\n        grpcOrderParam = SearchByTypeRequest.CategorySort.UNRECOGNIZED\n    ),\n    MostComment(\n        httpOrderParam = null,\n        grpcOrderParam = SearchByTypeRequest.CategorySort.CATEGORY_SORT_COMMENT_COUNT\n    ),\n    MostLikes(\n        httpOrderParam = null,\n        grpcOrderParam = SearchByTypeRequest.CategorySort.CATEGORY_SORT_LIKE_COUNT\n    );\n\n    companion object {\n        val webFilters =\n            listOf(ComprehensiveSort, MostClicks, LatestPublish, MostDanmaku, MostFavorites)\n        val allFilters =\n            listOf(ComprehensiveSort, MostClicks, LatestPublish, MostComment, MostLikes)\n    }\n}\n\nenum class SearchFilterDuration(\n    val httpDurationParam: Int?,\n    //val grpcOrderParam: SearchByTypeRequest.\n) {\n    All(null),\n    LessThan10Minutes(1),\n    Between10And30Minutes(2),\n    Between30And60Minutes(3),\n    MoreThan60Minutes(4);\n}\n\ndata class SearchTypeResult(\n    val videos: List<Video> = emptyList(),\n    val pgcs: List<Pgc> = emptyList(),\n    val users: List<User> = emptyList(),\n    val liveRooms: List<LiveRoom> = emptyList(),\n    val page: SearchTypePage,\n    val pageSize: Int? = 20\n) {\n    companion object {\n        fun fromSearchTypeResult(result: dev.aaa1115910.biliapi.http.entity.search.SearchResultData): SearchTypeResult {\n            return when (result.searchTypeResults.firstOrNull()) {\n                is dev.aaa1115910.biliapi.http.entity.search.SearchVideoResult -> {\n                    SearchTypeResult(\n                        videos = result.searchTypeResults.map { Video.fromSearchVideoResult(it as dev.aaa1115910.biliapi.http.entity.search.SearchVideoResult) },\n                        page = SearchTypePage(nextPageForWeb = result.page + 1),\n                        pageSize = result.pageSize\n                    )\n                }\n\n                is dev.aaa1115910.biliapi.http.entity.search.SearchMediaResult -> {\n                    SearchTypeResult(\n                        pgcs = result.searchTypeResults.map { Pgc.fromSearchPgcResult(it as dev.aaa1115910.biliapi.http.entity.search.SearchMediaResult) },\n                        page = SearchTypePage(nextPageForWeb = result.page + 1),\n                        pageSize = result.pageSize\n                    )\n                }\n\n                is dev.aaa1115910.biliapi.http.entity.search.SearchBiliUserResult -> {\n                    SearchTypeResult(\n                        users = result.searchTypeResults.map { User.fromSearchUserResult(it as dev.aaa1115910.biliapi.http.entity.search.SearchBiliUserResult) },\n                        page = SearchTypePage(nextPageForWeb = result.page + 1),\n                        pageSize = result.pageSize\n                    )\n                }\n\n                is dev.aaa1115910.biliapi.http.entity.search.SearchLiveRoomResult -> {\n                    SearchTypeResult(\n                        liveRooms = result.searchTypeResults.map { LiveRoom.fromSearchLiveRoomResult(it as dev.aaa1115910.biliapi.http.entity.search.SearchLiveRoomResult) },\n                        page = SearchTypePage(nextPageForWeb = result.page + 1),\n                        pageSize = result.pageSize\n                    )\n                }\n\n                else -> {\n                    SearchTypeResult(page = SearchTypePage(nextPageForWeb = result.page + 1), pageSize = result.pageSize)\n                }\n            }\n        }\n\n        fun fromSearchTypeResult(result: bilibili.polymer.app.search.v1.SearchByTypeResponse): SearchTypeResult {\n            return when (result.itemsList.firstOrNull()?.cardItemCase) {\n                bilibili.polymer.app.search.v1.Item.CardItemCase.AV -> {\n                    SearchTypeResult(\n                        videos = result.itemsList.map { Video.fromSearchVideoCard(it) },\n                        page = SearchTypePage(nextPageForApp = result.pagination.next)\n                    )\n                }\n\n                bilibili.polymer.app.search.v1.Item.CardItemCase.BANGUMI -> {\n                    SearchTypeResult(\n                        pgcs = result.itemsList.map { Pgc.fromSearchPgcCard(it) },\n                        page = SearchTypePage(nextPageForApp = result.pagination.next)\n                    )\n                }\n\n                bilibili.polymer.app.search.v1.Item.CardItemCase.AUTHOR -> {\n                    SearchTypeResult(\n                        users = result.itemsList.map { User.fromSearchUserCard(it) },\n                        page = SearchTypePage(nextPageForApp = result.pagination.next)\n                    )\n                }\n\n                else -> {\n                    SearchTypeResult(page = SearchTypePage(nextPageForApp = result.pagination.next))\n                }\n            }\n        }\n    }\n\n    interface SearchTypeResultItem\n\n    data class Video(\n        val aid: Long,\n        val bvid: String,\n        val title: String,\n        val cover: String,\n        val author: String,\n        val duration: Int,\n        val play: Long,\n        val danmaku: Int,\n        val pubTime: Int,\n        val pubDate: Int\n    ) : SearchTypeResultItem {\n        companion object {\n            fun fromSearchVideoResult(video: dev.aaa1115910.biliapi.http.entity.search.SearchVideoResult) =\n                Video(\n                    aid = video.aid,\n                    bvid = video.bvid,\n                    title = video.title,\n                    cover = \"https:${video.pic}\",\n                    author = video.author,\n                    duration = convertStringTimeToSeconds(video.duration),\n                    play = video.play,\n                    danmaku = video.danmaku,\n                    pubTime = video.pubDate,\n                    pubDate = video.pubDate\n                )\n\n            fun fromSearchVideoCard(video: bilibili.polymer.app.search.v1.Item) =\n                Video(\n                    aid = video.param.toLong(),\n                    bvid = video.av.share.video.bvid,\n                    title = video.av.title,\n                    cover = video.av.cover,\n                    author = video.av.author,\n                    duration = convertStringTimeToSeconds(video.av.duration),\n                    play = video.av.play,\n                    danmaku = video.av.danmaku,\n                    pubTime = video.av.ptime,\n                    pubDate = video.av.ptime\n                )\n        }\n    }\n\n    data class Pgc(\n        val title: String,\n        val cover: String,\n        val star: Float,\n        val seasonId: Int\n    ) : SearchTypeResultItem {\n        companion object {\n            fun fromSearchPgcResult(pgc: dev.aaa1115910.biliapi.http.entity.search.SearchMediaResult) =\n                Pgc(\n                    title = pgc.title,\n                    cover = pgc.cover,\n                    star = pgc.mediaScore.score,\n                    seasonId = pgc.seasonId\n                )\n\n            fun fromSearchPgcCard(pgc: bilibili.polymer.app.search.v1.Item) =\n                Pgc(\n                    title = pgc.bangumi.title,\n                    cover = pgc.bangumi.cover,\n                    star = pgc.bangumi.rating.toFloat(),\n                    seasonId = pgc.bangumi.seasonId.toInt()\n                )\n        }\n    }\n\n    data class User(\n        val mid: Long,\n        val name: String,\n        val avatar: String,\n        val sign: String\n    ) : SearchTypeResultItem {\n        companion object {\n            fun fromSearchUserResult(user: dev.aaa1115910.biliapi.http.entity.search.SearchBiliUserResult) =\n                User(\n                    mid = user.mid,\n                    name = user.uname,\n                    avatar = \"https:${user.upic}\",\n                    sign = user.usign\n                )\n\n            fun fromSearchUserCard(user: bilibili.polymer.app.search.v1.Item) =\n                User(\n                    mid = user.param.toLong(),\n                    name = user.author.title,\n                    avatar = user.author.cover,\n                    sign = user.author.sign\n                )\n        }\n    }\n\n    data class LiveRoom(\n        val roomId: Long,\n        val title: String,\n        val cover: String,\n        val uid: Long,\n        val uname: String,\n        val face: String,\n        val online: Int,\n        val liveStatus: Boolean,\n        val areaName: String? = null\n    ) : SearchTypeResultItem {\n        companion object {\n            fun fromSearchLiveRoomResult(liveRoom: dev.aaa1115910.biliapi.http.entity.search.SearchLiveRoomResult) =\n                LiveRoom(\n                    roomId = liveRoom.roomId,\n                    title = liveRoom.title,\n                    cover = \"https:${liveRoom.cover}\",\n                    uid = liveRoom.uid,\n                    uname = liveRoom.uname,\n                    face = liveRoom.uface,\n                    online = liveRoom.online,\n                    liveStatus = liveRoom.liveStatus == 1,\n                    areaName = liveRoom.cateName\n                )\n        }\n    }\n}\n\nprivate fun convertStringTimeToSeconds(time: String): Int {\n    val parts = time.split(\":\")\n    val hours = if (parts.size == 3) parts[0].toInt() else 0\n    val minutes = parts[parts.size - 2].toInt()\n    val seconds = parts[parts.size - 1].toInt()\n    return (hours * 3600) + (minutes * 60) + seconds\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/SeasonRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeason\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonData\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonStatus\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonType\nimport dev.aaa1115910.biliapi.entity.season.Timeline\nimport dev.aaa1115910.biliapi.entity.season.TimelineFilter\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.biliapi.http.util.BiliAppConf\nimport org.koin.core.annotation.Single\n\n@Single\nclass SeasonRepository(\n    private val authRepository: AuthRepository\n) {\n    /**\n     * 获取追番/追剧列表\n     *\n     * @param type 追番/追剧类型\n     * @param status 追剧状态 当 [preferApiType] == [ApiType.App] 时，不可使用 [FollowingSeasonStatus.All]\n     * @param pageNumber 页码\n     * @param pageSize 每页数量\n     * @param preferApiType 优先使用的 API 类型\n     */\n    suspend fun getFollowingSeasons(\n        type: FollowingSeasonType = FollowingSeasonType.Bangumi,\n        status: FollowingSeasonStatus = FollowingSeasonStatus.All,\n        pageNumber: Int = 1,\n        pageSize: Int = 30,\n        preferApiType: ApiType = ApiType.Web\n    ): FollowingSeasonData {\n        return when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.getFollowingSeasons(\n                type = type.id,\n                status = status.id,\n                pageNumber = pageNumber,\n                pageSize = pageSize,\n                mid = authRepository.mid!!,\n                sessData = authRepository.sessionData\n            ).getResponseData()\n                .let { responseData ->\n                    FollowingSeasonData(\n                        list = responseData.list.map { FollowingSeason.fromFollowingSeason(it) },\n                        total = responseData.total\n                    )\n                }\n\n            ApiType.App -> BiliHttpApi.getFollowingSeasons(\n                type = type.paramName,\n                status = status.id,\n                pageNumber = pageNumber,\n                pageSize = pageSize,\n                build = BiliAppConf.APP_BUILD_CODE,\n                accessKey = authRepository.accessToken!!\n            ).getResponseData()\n                .let { responseData ->\n                    FollowingSeasonData(\n                        list = responseData.followList.map { FollowingSeason.fromFollowingSeason(it) },\n                        total = responseData.total\n                    )\n                }\n        }\n    }\n\n    suspend fun getTimeline(\n        filter: TimelineFilter = TimelineFilter.All,\n        preferApiType: ApiType = ApiType.Web\n    ): List<Timeline> {\n        return when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.getTimeline(\n                type = filter.webFilterId,\n                before = 7,\n                after = 7\n            ).getResponseData().map { Timeline.fromTimeline(it) }\n\n            ApiType.App -> BiliHttpApi.getTimeline(\n                filterType = filter.appFilterId\n            ).getResponseData().data.map { Timeline.fromTimeline(it) }\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/ToViewRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport bilibili.app.interfaces.v1.HistoryGrpcKt\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.user.ToViewData\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport org.koin.core.annotation.Single\n\n@Single\nclass ToViewRepository(\n    private val authRepository: AuthRepository,\n    private val channelRepository: ChannelRepository\n) {\n    private val historyStub\n        get() = runCatching {\n            HistoryGrpcKt.HistoryCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n\n    suspend fun getToView(\n        cursor: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): ToViewData {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val data = BiliHttpApi.getToView(\n                    // viewAt = cursor,\n                    sessData = authRepository.sessionData!!,\n                ).getResponseData()\n                print(data)\n                ToViewData.fromToViewResponse(data)\n            }\n\n            ApiType.App -> {\n                val data = BiliHttpApi.getToView(\n                    // viewAt = cursor,\n                    sessData = authRepository.sessionData!!,\n                ).getResponseData()\n                ToViewData.fromToViewResponse(data)\n            }\n        }\n    }\n\n    suspend fun deleteToView(\n        avid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean {\n        return runCatching {\n            BiliHttpApi.deleteToView(\n                avid = avid,\n                csrf = authRepository.biliJct!!,\n                sessData = authRepository.sessionData!!\n            ).code == 0\n        }.getOrDefault(false)\n    }\n\n    suspend fun clearToView(\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean {\n        return runCatching {\n            when (preferApiType) {\n                ApiType.Web, ApiType.App -> {\n                    BiliHttpApi.clearToView(\n                        csrf = authRepository.biliJct!!,\n                        sessData = authRepository.sessionData!!\n                    ).code == 0\n                }\n            }\n        }.getOrDefault(false)\n    }\n\n    suspend fun addToView(\n        avid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean {\n        return runCatching {\n            when (preferApiType) {\n                ApiType.Web, ApiType.App -> {\n                    BiliHttpApi.addToView(\n                        avid = avid,\n                        csrf = authRepository.biliJct!!,\n                        sessData = authRepository.sessionData!!\n                    ).code == 0\n                }\n            }\n        }.getOrDefault(false)\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/UgcRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.CarouselData\nimport dev.aaa1115910.biliapi.entity.ugc.UgcType\nimport dev.aaa1115910.biliapi.entity.ugc.UgcTypeV2\nimport dev.aaa1115910.biliapi.entity.ugc.region.UgcFeedData\nimport dev.aaa1115910.biliapi.entity.ugc.region.UgcFeedPage\nimport dev.aaa1115910.biliapi.entity.ugc.region.UgcRegionData\nimport dev.aaa1115910.biliapi.entity.ugc.region.UgcRegionListData\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport org.koin.core.annotation.Single\n\n@Single\nclass UgcRepository(\n    private val authRepository: AuthRepository\n) {\n    private val logger = KotlinLogging.logger {}\n\n    suspend fun getCarousel(ugcType: UgcTypeV2): CarouselData {\n        return runCatching {\n            val regionBanner = BiliHttpApi.getRegionBanner(ugcType.tid).getResponseData()\n            CarouselData.fromRegionBanner(regionBanner)\n        }.getOrElse {\n            logger.warn(it) { \"load $ugcType carousel failed, fallback to empty carousel\" }\n            CarouselData(emptyList())\n        }\n    }\n\n    @Deprecated(\"User getRegionFeedRcmd instead\")\n    suspend fun getRegionData(ugcType: UgcType): UgcRegionData {\n        val responseData = BiliHttpApi.getRegionDynamic(\n            rid = ugcType.rid,\n            accessKey = authRepository.accessToken ?: \"\",\n        ).getResponseData()\n        val data = UgcRegionData.fromRegionDynamic(responseData)\n        return data\n    }\n\n    @Deprecated(\"User getRegionFeedRcmd instead\")\n    suspend fun getRegionMoreData(ugcType: UgcType): UgcRegionListData {\n        val responseData = BiliHttpApi.getRegionDynamicList(\n            rid = ugcType.rid,\n            accessKey = authRepository.accessToken ?: \"\",\n        ).getResponseData()\n        val data = UgcRegionListData.fromRegionDynamicList(responseData)\n        return data\n    }\n\n    suspend fun getRegionFeedRcmd(ugcType: UgcTypeV2, page: UgcFeedPage): UgcFeedData {\n        val responseData = BiliHttpApi.getRegionFeedRcmd(\n            displayId = page.nextPage,\n            fromRegion = ugcType.tid,\n            sessData = authRepository.sessionData\n        ).getResponseData()\n        val ugcFeedData = UgcFeedData.fromRegionFeedRcmd(responseData)\n        ugcFeedData.nextPage = UgcFeedPage(page.nextPage + 1)\n        return ugcFeedData\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/UserRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport bilibili.app.dynamic.v2.DynamicGrpcKt\nimport bilibili.app.dynamic.v2.Refresh\nimport bilibili.app.dynamic.v2.UserInfo\nimport bilibili.app.dynamic.v2.dynAllReq\nimport bilibili.app.dynamic.v2.dynDetailReq\nimport bilibili.app.dynamic.v2.dynVideoReq\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.user.DynamicData\nimport dev.aaa1115910.biliapi.entity.user.DynamicItem\nimport dev.aaa1115910.biliapi.entity.user.DynamicVideoData\nimport dev.aaa1115910.biliapi.entity.user.FollowedUser\nimport dev.aaa1115910.biliapi.entity.user.SpaceVideoData\nimport dev.aaa1115910.biliapi.entity.user.SpaceVideoOrder\nimport dev.aaa1115910.biliapi.entity.user.SpaceVideoPage\nimport dev.aaa1115910.biliapi.grpc.utils.handleGrpcException\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.biliapi.http.entity.user.FollowAction\nimport dev.aaa1115910.biliapi.http.entity.user.FollowActionSource\nimport dev.aaa1115910.biliapi.http.entity.user.RelationType\nimport dev.aaa1115910.biliapi.http.entity.user.UserCardData\nimport dev.aaa1115910.biliapi.http.entity.user.UserInfoData\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.withContext\nimport org.koin.core.annotation.Single\nimport kotlin.math.ceil\n\n@Single\nclass UserRepository(\n    private val authRepository: AuthRepository,\n    private val channelRepository: ChannelRepository\n) {\n    private val dynamicStub\n        get() = runCatching {\n            DynamicGrpcKt.DynamicCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n\n    private suspend fun modifyFollow(\n        mid: Long,\n        action: FollowAction,\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean {\n        val response = when (preferApiType) {\n            ApiType.Web -> {\n                BiliHttpApi.modifyFollow(\n                    mid = mid,\n                    action = action,\n                    actionSource = FollowActionSource.Space,\n                    csrf = authRepository.biliJct,\n                    sessData = authRepository.sessionData\n                )\n            }\n\n            ApiType.App -> {\n                BiliHttpApi.modifyFollow(\n                    mid = mid,\n                    action = action,\n                    actionSource = FollowActionSource.Space,\n                    accessKey = authRepository.accessToken\n                )\n            }\n        }\n        return response.code == 0\n    }\n\n    suspend fun followUser(\n        mid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean = modifyFollow(mid, FollowAction.AddFollow, preferApiType)\n\n    suspend fun unfollowUser(\n        mid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean = modifyFollow(mid, FollowAction.DelFollow, preferApiType)\n\n    suspend fun checkIsFollowing(\n        mid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): Boolean? {\n        if (authRepository.sessionData == null && authRepository.accessToken == null) return null\n        return runCatching {\n            val response = when (preferApiType) {\n                ApiType.Web -> {\n                    BiliHttpApi.getRelations(\n                        mid = mid,\n                        sessData = authRepository.sessionData\n                    )\n                }\n\n                ApiType.App -> {\n                    BiliHttpApi.getRelations(\n                        mid = mid,\n                        //移动端貌似并没有使用这个接口，目前该接口返回-663鉴权失败，直接改用sessdata获取\n                        sessData = authRepository.sessionData\n                        //accessKey = authRepository.accessToken\n                    )\n                }\n            }.getResponseData()\n            listOf(\n                RelationType.Followed,\n                RelationType.FollowedQuietly,\n                RelationType.BothFollowed\n            ).contains(response.relation.attribute)\n        }.onFailure {\n            it.printStackTrace()\n        }.getOrNull()\n    }\n\n    //TODO 改成返回 关注数，粉丝数，黑名单数\n    suspend fun getFollowingUpCount(\n        mid: Long,\n        preferApiType: ApiType\n    ): Int {\n        if (authRepository.sessionData == null && authRepository.accessToken == null) return 0\n        return runCatching {\n            val response = when (preferApiType) {\n                ApiType.Web -> {\n                    BiliHttpApi.getRelationStat(\n                        mid = mid,\n                        sessData = authRepository.sessionData\n                    )\n                }\n\n                ApiType.App -> {\n                    BiliHttpApi.getRelationStat(\n                        mid = mid,\n                        accessKey = authRepository.accessToken\n                    )\n                }\n            }.getResponseData()\n            response.following\n        }.onFailure {\n            it.printStackTrace()\n        }.getOrNull() ?: 0\n    }\n\n    suspend fun addSeasonFollow(\n        seasonId: Int,\n        preferApiType: ApiType = ApiType.Web\n    ): String {\n        return when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.addSeasonFollow(\n                seasonId = seasonId,\n                csrf = authRepository.biliJct!!,\n                sessData = authRepository.sessionData!!\n            )\n\n            ApiType.App -> BiliHttpApi.addSeasonFollow(\n                seasonId = seasonId,\n                accessKey = authRepository.accessToken!!\n            )\n        }.getResponseData().toast\n    }\n\n    suspend fun delSeasonFollow(\n        seasonId: Int,\n        preferApiType: ApiType = ApiType.Web\n    ): String {\n        return when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.delSeasonFollow(\n                seasonId = seasonId,\n                csrf = authRepository.biliJct!!,\n                sessData = authRepository.sessionData!!\n            )\n\n            ApiType.App -> BiliHttpApi.delSeasonFollow(\n                seasonId = seasonId,\n                accessKey = authRepository.accessToken!!\n            )\n        }.getResponseData().toast\n    }\n\n    suspend fun getSpaceVideos(\n        mid: Long,\n        order: SpaceVideoOrder = SpaceVideoOrder.PubDate,\n        page: SpaceVideoPage = SpaceVideoPage(),\n        preferApiType: ApiType = ApiType.Web\n    ): SpaceVideoData {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val webSpaceVideoData = BiliHttpApi.getWebUserSpaceVideos(\n                    mid = mid,\n                    order = order.value,\n                    pageNumber = page.nextWebPageNumber,\n                    pageSize = page.nextWebPageSize,\n                    sessData = authRepository.sessionData ?: \"\",\n                    dedeUserID = authRepository.mid\n                ).getResponseData()\n                SpaceVideoData.fromWebSpaceVideoData(webSpaceVideoData)\n            }\n\n            ApiType.App -> {\n                val appSpaceVideoData = BiliHttpApi.getAppUserSpaceVideos(\n                    mid = mid,\n                    lastAvid = page.lastAvid,\n                    order = order.value,\n                    ts = System.currentTimeMillis(),\n                    accessKey = authRepository.accessToken ?: \"\"\n                ).getResponseData()\n                SpaceVideoData.fromAppSpaceVideoData(appSpaceVideoData)\n            }\n        }\n    }\n\n    suspend fun getDynamicVideos(\n        page: Int,\n        offset: String,\n        updateBaseline: String,\n        preferApiType: ApiType = ApiType.Web\n    ): DynamicVideoData {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val responseData = BiliHttpApi.getDynamicList(\n                    type = \"video\",\n                    page = page,\n                    offset = offset,\n                    sessData = authRepository.sessionData ?: \"\"\n                ).getResponseData()\n                DynamicVideoData.fromDynamicData(responseData)\n            }\n\n            ApiType.App -> {\n                var result: DynamicVideoData? = null\n                runCatching {\n                    val dynVideoReply = dynamicStub?.dynVideo(dynVideoReq {\n                        this.page = page\n                        this.offset = offset\n                        this.updateBaseline = updateBaseline\n                        localTime = 8\n                        refreshType =\n                            if (offset == \"\") Refresh.refresh_new else Refresh.refresh_history\n                    })\n                    result = DynamicVideoData.fromDynamicData(dynVideoReply!!)\n                }.onFailure {\n                    handleGrpcException(it)\n                }\n                result!!\n            }\n        }\n    }\n\n    suspend fun getDynamics(\n        page: Int,\n        offset: String,\n        updateBaseline: String,\n        preferApiType: ApiType = ApiType.Web\n    ): DynamicData {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val responseData = BiliHttpApi.getDynamicList(\n                    type = \"all\",\n                    page = page,\n                    offset = offset,\n                    sessData = authRepository.sessionData ?: \"\"\n                ).getResponseData()\n                DynamicData.fromDynamicData(responseData)\n            }\n\n            ApiType.App -> {\n                var result: DynamicData? = null\n                runCatching {\n                    val dynAllReply = dynamicStub?.dynAll(dynAllReq {\n                        this.page = page\n                        this.offset = offset\n                        this.updateBaseline = updateBaseline\n                        localTime = 8\n                        refreshType =\n                            if (offset == \"\") Refresh.refresh_new else Refresh.refresh_history\n                    })\n                    result = DynamicData.fromDynamicData(dynAllReply!!)\n                }.onFailure {\n                    handleGrpcException(it)\n                }\n                result!!\n            }\n        }\n    }\n\n    suspend fun getDynamicDetail(\n        dynamicId: String,\n        preferApiType: ApiType = ApiType.Web\n    ): DynamicItem {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val responseData = BiliHttpApi.getDynamicDetail(\n                    id = dynamicId,\n                    features = \"itemOpusStyle\",\n                    sessData = authRepository.sessionData ?: \"\"\n                ).getResponseData()\n                DynamicItem.fromDynamicItem(responseData.item)\n            }\n\n            ApiType.App -> {\n                var result: DynamicItem? = null\n                runCatching {\n                    val dynDetailReply = dynamicStub?.dynDetail(dynDetailReq {\n                        this.dynamicId = dynamicId\n                        localTime = 8\n                    })\n                    result = DynamicItem.fromDynamicItem(dynDetailReply!!.item)\n                }.onFailure {\n                    handleGrpcException(it)\n                }\n                result!!\n            }\n        }\n    }\n\n    suspend fun getFollowedUsers(\n        mid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): List<FollowedUser> {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val result = mutableListOf<FollowedUser>()\n                val firstResponse = BiliHttpApi.getUserFollow(\n                    mid = mid,\n                    sessData = authRepository.sessionData!!\n                ).getResponseData()\n                val userCount = firstResponse.total\n                val pageCount = ceil((userCount.toFloat() / 50)).toInt()\n                result.addAll(firstResponse.list.map { FollowedUser.fromHttpFollowedUser(it) })\n                withContext(Dispatchers.IO) {\n                    (2..pageCount).map { pageNumber ->\n                        async {\n                            BiliHttpApi.getUserFollow(\n                                mid = mid,\n                                pageNumber = pageNumber,\n                                sessData = authRepository.sessionData!!\n                            ).getResponseData()\n                        }\n                    }.awaitAll().forEach { userFollowData ->\n                        result.addAll(userFollowData.list.map { FollowedUser.fromHttpFollowedUser(it) })\n                    }\n                }\n                result\n            }\n\n            ApiType.App -> {\n                val result = mutableListOf<FollowedUser>()\n                val firstResponse = BiliHttpApi.getUserFollow(\n                    mid = mid,\n                    accessKey = authRepository.accessToken!!\n                ).getResponseData()\n                val userCount = firstResponse.total\n                val pageCount = ceil((userCount.toFloat() / 50)).toInt()\n                result.addAll(firstResponse.list.map { FollowedUser.fromHttpFollowedUser(it) })\n                withContext(Dispatchers.IO) {\n                    (2..pageCount).map { pageNumber ->\n                        async {\n                            BiliHttpApi.getUserFollow(\n                                mid = mid,\n                                pageNumber = pageNumber,\n                                accessKey = authRepository.accessToken!!\n                            ).getResponseData()\n                        }\n                    }.awaitAll().forEach { userFollowData ->\n                        result.addAll(userFollowData.list.map { FollowedUser.fromHttpFollowedUser(it) })\n                    }\n                }\n                result\n            }\n        }\n    }\n    \n    suspend fun getUserInfo(mid: Long): UserInfoData {\n        val response = BiliHttpApi.getUserInfo(\n            uid = mid,\n            sessData = authRepository.sessionData ?: \"\"\n        ).getResponseData()\n        return response\n    }\n\n    suspend fun getUserCardInfo(mid: Long): UserCardData {\n        val response = BiliHttpApi.getUserCardInfo(\n            mid = mid,\n            sessData = authRepository.sessionData ?: \"\"\n        ).getResponseData()\n        return response\n    }\n}"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/VideoDetailRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport bilibili.app.view.v1.ViewGrpcKt\nimport bilibili.app.view.v1.viewReq\nimport bilibili.main.community.reply.v1.Mode\nimport bilibili.main.community.reply.v1.ReplyGrpcKt\nimport bilibili.main.community.reply.v1.detailListReq\nimport bilibili.main.community.reply.v1.mainListReq\nimport bilibili.pagination.feedPagination\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.reply.CommentPage\nimport dev.aaa1115910.biliapi.entity.reply.CommentRepliesData\nimport dev.aaa1115910.biliapi.entity.reply.CommentReplyPage\nimport dev.aaa1115910.biliapi.entity.reply.CommentSort\nimport dev.aaa1115910.biliapi.entity.reply.CommentsData\nimport dev.aaa1115910.biliapi.entity.video.VideoDetail\nimport dev.aaa1115910.biliapi.entity.video.season.SeasonDetail\nimport dev.aaa1115910.biliapi.grpc.utils.handleGrpcException\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.biliapi.http.entity.user.garb.EquipPart\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.json.Json\nimport org.koin.core.annotation.Single\n\n@Single\nclass VideoDetailRepository(\n    private val authRepository: AuthRepository,\n    private val channelRepository: ChannelRepository,\n    private val favoriteRepository: FavoriteRepository,\n    private val likeRepository: LikeRepository,\n    private val coinRepository: CoinRepository\n) {\n    private val viewStub\n        get() = runCatching {\n            ViewGrpcKt.ViewCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n    private val replyStub\n        get() = runCatching {\n            ReplyGrpcKt.ReplyCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n\n    suspend fun getVideoDetail(\n        aid: Long,\n        preferApiType: ApiType = ApiType.Web,\n        withUserActions: Boolean = true\n    ): VideoDetail {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                withContext(Dispatchers.IO) {\n                    // 串行执行：获取视频详情\n                    val videoDetailWithoutUserActions = run {\n                        val httpVideoDetail = BiliHttpApi.getVideoDetail(\n                            av = aid,\n                            sessData = authRepository.sessionData ?: \"\"\n                        ).getResponseData()\n                        VideoDetail.fromVideoDetail(httpVideoDetail)\n                    }\n\n                    // 声明变量\n                    var isLiked = false\n                    var isFavoured = false\n                    var isCoined = false\n\n//                    if (withUserActions) {\n//                        // 检查点赞、收藏、投币状态\n//                        runCatching {\n//                            val archiveRelation = BiliHttpApi.getArchiveRelation(\n//                                avid = aid,\n//                                sessData = authRepository.sessionData ?: \"\"\n//                            ).getResponseData()\n//                            isLiked = archiveRelation.like\n//                            isCoined = archiveRelation.coin > 0\n//                            isFavoured = archiveRelation.favorite\n//                        }.onFailure {\n//                            println(\"Check video relation failed: $it\")\n//                        }\n//                    }\n                    if (withUserActions) {\n                        // 串行执行：检查点赞状态\n                        isLiked = runCatching {\n                            likeRepository.checkVideoLike(\n                                aid = aid,\n                                preferApiType = ApiType.Web\n                            )\n                        }.onFailure {\n                            println(\"Check video liked failed: $it\")\n                        }.getOrDefault(false)\n\n                        // 串行执行：检查投币状态\n                        isCoined =  runCatching {\n                            coinRepository.checkVideoCoin(\n                                aid = aid,\n                                preferApiType = ApiType.Web\n                            )\n                        }.onFailure {\n                            println(\"Check video liked failed: $it\")\n                        }.getOrDefault(false)\n\n                        // 串行执行：检查收藏状态\n                        isFavoured = runCatching {\n                            favoriteRepository.checkVideoFavoured(\n                                aid = aid,\n                                preferApiType = ApiType.Web\n                            )\n                        }.onFailure {\n                            println(\"Check video favoured failed: $it\")\n                        }.getOrDefault(false)\n                    }\n\n                    // 串行执行：获取历史和播放器图标\n                    val (history, playerIcon) = runCatching {\n                        val videoModeInfo = BiliHttpApi.getVideoMoreInfo(\n                            avid = aid,\n                            cid = videoDetailWithoutUserActions.cid,\n                            sessData = authRepository.sessionData ?: \"\",\n                            buvid3 = authRepository.buvid3 ?: \"\"\n                        ).getResponseData()\n                        val history = VideoDetail.History(\n                            progress = videoModeInfo.lastPlayTime / 1000,\n                            lastPlayedCid = videoModeInfo.lastPlayCid\n                        )\n                        val playerIcon =\n                            VideoDetail.PlayerIcon.fromPlayerIcon(videoModeInfo.playerIcon)\n                        history to playerIcon\n                    }.onFailure {\n                        println(\"Get video history failed: $it\")\n                    }.getOrDefault(VideoDetail.History(0, 0) to null)\n\n                    // 更新并返回结果\n                    videoDetailWithoutUserActions.apply {\n                        userActions.like = isLiked\n                        userActions.coin = isCoined\n                        userActions.favorite = isFavoured\n                        this.history = history\n                        this.playerIcon = playerIcon\n                    }\n                }\n            }\n\n            ApiType.App -> {\n                val viewReply = runCatching {\n                    viewStub?.view(viewReq {\n                        this.aid = aid\n                    }) ?: throw IllegalStateException(\"Player stub is not initialized\")\n                }.onFailure { handleGrpcException(it) }.getOrThrow()\n                VideoDetail.fromViewReply(viewReply).apply {\n                    if (playerIcon?.idle?.isBlank() != false && authRepository.sessionData != null) {\n                        println(\"player icon not found in view reply, try to get it from garb api\")\n                        runCatching {\n                            val playerIconGarb = BiliHttpApi.getUserEquippedGarb(\n                                part = EquipPart.PlayerIcon,\n                                sessData = authRepository.sessionData!!\n                            ).getResponseData()\n                            val playerIconItem = playerIconGarb.item\n                                ?: throw IllegalStateException(\"player icon not equipped\")\n                            this.playerIcon = VideoDetail.PlayerIcon(\n                                idle = playerIconItem.properties.icon ?: \"\",\n                                moving = playerIconItem.properties.dragIcon ?: \"\"\n                            )\n                        }.onFailure {\n                            println(\"Get player icon failed: $it\")\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    suspend fun getPgcVideoDetail(\n        epid: Int? = null,\n        seasonId: Int? = null,\n        preferApiType: ApiType = ApiType.Web\n    ): SeasonDetail {\n        when (preferApiType) {\n            ApiType.Web -> {\n                val webSeasonData = BiliHttpApi.getWebSeasonInfo(\n                    epId = epid,\n                    seasonId = seasonId,\n                    sessData = authRepository.sessionData ?: \"\"\n                ).getResponseData()\n                webSeasonData.userStatus = BiliHttpApi.getSeasonUserStatus(\n                    seasonId = seasonId!!,\n                    sessData = authRepository.sessionData ?: \"\"\n                ).getResponseData()\n                val seasonDetail = SeasonDetail.fromSeasonData(webSeasonData)\n                val firstEp = webSeasonData.episodes.firstOrNull() ?: return seasonDetail\n\n                val playerIcon = runCatching {\n                    val videoModeInfo = BiliHttpApi.getVideoMoreInfo(\n                        avid = firstEp.aid,\n                        cid = firstEp.cid,\n                        sessData = authRepository.sessionData ?: \"\",\n                        buvid3 = authRepository.buvid3 ?: \"\"\n                    ).getResponseData()\n                    val playerIcon = VideoDetail.PlayerIcon.fromPlayerIcon(videoModeInfo.playerIcon)\n                    playerIcon\n                }.onFailure {\n                    println(\"Get video player icon failed: $it\")\n                }.getOrDefault(null)\n                seasonDetail.playerIcon = playerIcon\n                return seasonDetail\n            }\n\n            ApiType.App -> {\n                val appSeasonData = BiliHttpApi.getAppSeasonInfo(\n                    epId = epid,\n                    seasonId = seasonId,\n                    mobiApp = \"android_hd\",\n                    accessKey = authRepository.accessToken ?: \"\"\n                ).getResponseData()\n                return SeasonDetail.fromSeasonData(appSeasonData)\n            }\n        }\n    }\n\n    suspend fun getComments(\n        aid: Long,\n        sort: CommentSort = CommentSort.Hot,\n        page: CommentPage = CommentPage(),\n        preferApiType: ApiType = ApiType.Web\n    ): CommentsData {\n        when (preferApiType) {\n            ApiType.Web -> {\n                val webComments = BiliHttpApi.getComments(\n                    oid = aid,\n                    type = 1,\n                    mode = sort.param,\n                    paginationStr = Json.encodeToString(mapOf(\"offset\" to page.nextWebPage)),\n                    sessData = authRepository.sessionData ?: \"\",\n                    dedeUserID = authRepository.mid,\n                    buvid3 = authRepository.buvid3 ?: \"\"\n                ).getResponseData()\n                return CommentsData.fromCommentData(webComments)\n            }\n\n            ApiType.App -> {\n                val appComments = replyStub?.mainList(\n                    mainListReq {\n                        oid = aid\n                        type = 1\n                        /*cursor = cursorReq {\n                            next = page.nextAppPage.toLong()\n                            mode = when (sort) {\n                                CommentSort.Hot -> Mode.MAIN_LIST_HOT\n                                CommentSort.HotAndTime -> Mode.DEFAULT\n                                CommentSort.Time -> Mode.MAIN_LIST_TIME\n                            }\n                        }*/\n                        mode = when (sort) {\n                            CommentSort.Hot -> Mode.MAIN_LIST_HOT\n                            CommentSort.HotAndTime -> Mode.DEFAULT\n                            CommentSort.Time -> Mode.MAIN_LIST_TIME\n                        }\n                        pagination = feedPagination {\n                            offset = page.nextAppPage\n                        }\n                    }\n                ) ?: throw IllegalStateException(\"Reply stub is not initialized\")\n                return CommentsData.fromMainListReply(appComments)\n            }\n        }\n    }\n\n    suspend fun getCommentReplies(\n        aid: Long,\n        commentId: Long,\n        page: CommentReplyPage = CommentReplyPage(),\n        sort: CommentSort = CommentSort.Hot,\n        preferApiType: ApiType = ApiType.Web\n    ): CommentRepliesData {\n        when (preferApiType) {\n            ApiType.Web -> {\n                val webReplies = BiliHttpApi.getCommentReplies(\n                    oid = aid,\n                    type = 1,\n                    root = commentId,\n                    pageNumber = page.nextWebPage,\n                    sessData = authRepository.sessionData ?: \"\",\n                    dedeUserID = authRepository.mid,\n                    buvid3 = authRepository.buvid3 ?: \"\"\n                ).getResponseData()\n                return CommentRepliesData.fromCommentReplyData(webReplies)\n            }\n\n            ApiType.App -> {\n                val appReplies = replyStub?.detailList(\n                    detailListReq {\n                        oid = aid\n                        type = 1\n                        root = commentId\n                        /*cursor = cursorReq {\n                            next = page.nextAppPage.toLong()\n                        }*/\n                        mode = when (sort) {\n                            CommentSort.Hot -> Mode.MAIN_LIST_HOT\n                            CommentSort.HotAndTime -> Mode.DEFAULT\n                            CommentSort.Time -> Mode.MAIN_LIST_TIME\n                        }\n                        pagination = feedPagination {\n                            offset = page.nextAppPage\n                        }\n                    }\n                ) ?: throw IllegalStateException(\"Reply stub is not initialized\")\n                return CommentRepliesData.fromCommentReplyList(appReplies)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/repositories/VideoPlayRepository.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport bilibili.app.playerunite.v1.PlayerGrpcKt\nimport bilibili.app.playerunite.v1.playViewUniteReq\nimport bilibili.community.service.dm.v1.DMGrpcKt\nimport bilibili.community.service.dm.v1.dmViewReq\nimport bilibili.pgc.gateway.player.v2.playViewReq\nimport bilibili.playershared.videoVod\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.CodeType\nimport dev.aaa1115910.biliapi.entity.PlayData\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMask\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskSegment\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskType\nimport dev.aaa1115910.biliapi.entity.video.HeartbeatVideoType\nimport dev.aaa1115910.biliapi.entity.video.Subtitle\nimport dev.aaa1115910.biliapi.entity.video.VideoShot\nimport dev.aaa1115910.biliapi.grpc.utils.handleGrpcException\nimport dev.aaa1115910.biliapi.http.BiliHttpApi\nimport dev.aaa1115910.biliapi.http.BiliHttpProxyApi\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.withContext\nimport org.koin.core.annotation.Single\nimport bilibili.pgc.gateway.player.v2.PlayURLGrpcKt as PgcPlayURLGrpcKt\n\n@Single\nclass VideoPlayRepository(\n    private val authRepository: AuthRepository,\n    private val channelRepository: ChannelRepository\n) {\n    private val playerStub\n        get() = runCatching {\n            PlayerGrpcKt.PlayerCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n    private val pgcPlayUrlStub\n        get() = runCatching {\n            PgcPlayURLGrpcKt.PlayURLCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n    private val danmakuStub\n        get() = runCatching {\n            DMGrpcKt.DMCoroutineStub(channelRepository.defaultChannel!!)\n        }.getOrNull()\n\n    private val proxyPgcPlayUrlStub\n        get() = runCatching {\n            PgcPlayURLGrpcKt.PlayURLCoroutineStub(channelRepository.proxyChannel!!)\n        }.getOrNull()\n\n\n    suspend fun getPlayData(\n        aid: Long,\n        cid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): PlayData {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val playUrlData = BiliHttpApi.getVideoPlayUrl(\n                    av = aid,\n                    cid = cid,\n                    fnval = 4048,\n                    qn = 127,\n                    fnver = 0,\n                    fourk = 1,\n                    sessData = authRepository.sessionData,\n                    dedeUserID = authRepository.mid,\n                    gaiaVtoken = authRepository.gaiaVtoken\n                ).getResponseData()\n                PlayData.fromPlayUrlData(playUrlData)\n            }\n\n            ApiType.App -> {\n                withContext(Dispatchers.IO) {\n                    val codecTypes = listOf(\n                        CodeType.Code264,\n                        CodeType.Code265,\n                        CodeType.CodeAv1\n                    )\n                    val replies = codecTypes.map { codecType ->\n                        async {\n                            val playUniteReply = runCatching {\n                                playerStub?.playViewUnite(playViewUniteReq {\n                                    vod = videoVod {\n                                        this.aid = aid\n                                        this.cid = cid\n                                        fnval = 4048\n                                        qn = 127\n                                        fnver = 0\n                                        fourk = true\n                                        preferCodecType = codecType.toPlayerSharedCodeType()\n                                    }\n                                }) ?: throw IllegalStateException(\"Player stub is not initialized\")\n                            }.onFailure {\n                                // dont throw\n                                runCatching { handleGrpcException(it) }\n                                    .onFailure {\n                                        println(\"get play data failed: [aid=$aid, cid=$cid, preferCodec=$codecType, preferApiType=$preferApiType]\")\n                                        it.printStackTrace()\n                                    }\n                            }.getOrNull()\n                            playUniteReply\n                        }\n                    }.awaitAll()\n                    val result = replies.map {\n                        it?.let { PlayData.fromPlayViewUniteReply(it) }\n                    }.reduce { acc, playData ->\n                        acc?.let { playData?.let { acc + playData } ?: acc } ?: playData\n                    } ?: throw IllegalStateException(\"All codec types are failed to get play data\")\n                    result\n                }\n            }\n        }\n    }\n\n    suspend fun getPgcPlayData(\n        aid: Long?,\n        cid: Long?,\n        epid: Int,\n        preferCodec: CodeType = CodeType.NoCode,\n        preferApiType: ApiType = ApiType.Web,\n        enableProxy: Boolean = false,\n        proxyArea: String = \"\"\n    ): PlayData {\n        println(\"get pgc play data: [aid=$aid, cid=$cid, epid=$epid, preferCodec=$preferCodec, preferApiType=$preferApiType, enableProxy=$enableProxy, proxyArea=$proxyArea]\")\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val playUrlData = if (enableProxy) {\n                    BiliHttpProxyApi.getPgcVideoPlayUrlV2(\n                        av = aid,\n                        cid = cid,\n                        epid = epid,\n                        fnval = 4048,\n                        qn = 127,\n                        fnver = 0,\n                        fourk = 1,\n                        sessData = authRepository.sessionData\n//                        buvid3 = authRepository.buvid3\n                    )\n                } else {\n                    BiliHttpApi.getPgcVideoPlayUrlV2(\n                        av = aid,\n                        cid = cid,\n                        epid = epid,\n                        fnval = 4048,\n                        qn = 127,\n                        fnver = 0,\n                        fourk = 1,\n                        sessData = authRepository.sessionData,\n                        gaiaVtoken = authRepository.gaiaVtoken\n//                        buvid3 = authRepository.buvid3\n                    )\n                }.getResponseData()\n\n                PlayData.fromPlayUrlV2Data(playUrlData)\n            }\n\n            ApiType.App -> {\n                withContext(Dispatchers.IO) {\n                    val codecTypes = listOf(\n                        CodeType.Code264,\n                        CodeType.Code265,\n                        CodeType.CodeAv1\n                    )\n                    val replies = codecTypes.map { codecType ->\n                        val req = playViewReq {\n                            this.epid = epid.toLong()\n                            cid?.let { this.cid = it }\n                            qn = 127\n                            fnver = 0\n                            fnval = 4048\n                            fourk = true\n                            forceHost = 0\n                            download = 0\n                            preferCodecType = codecType.toPgcPlayUrlCodeType()\n                        }\n                        async {\n                            val playReply = runCatching {\n                                if (enableProxy) {\n                                    proxyPgcPlayUrlStub?.playView(req)\n                                        ?: throw IllegalStateException(\"Proxy pgc play url stub is not initialized\")\n                                } else {\n                                    pgcPlayUrlStub?.playView(req)\n                                        ?: throw IllegalStateException(\"Pgc play url stub is not initialized\")\n                                }\n                            }.onFailure {\n                                // dont throw\n                                runCatching { handleGrpcException(it) }\n                                    .onFailure {\n                                        println(\"get pgc play data failed: [aid=$aid, cid=$cid, epid=$epid, preferCodec=$codecType, preferApiType=$preferApiType]\")\n                                        it.printStackTrace()\n                                    }\n                            }.getOrNull()\n                            playReply\n                        }\n                    }.awaitAll()\n                    val result = replies.map {\n                        it?.let { PlayData.fromPgcPlayViewReply(it) }\n                    }.reduce { acc, playData ->\n                        acc?.let { playData?.let { acc + playData } ?: acc } ?: playData\n                    } ?: throw IllegalStateException(\"All codec types are failed to get play data\")\n                    result\n                }\n            }\n        }\n    }\n\n    suspend fun getSubtitle(\n        aid: Long,\n        cid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): List<Subtitle> {\n        return when (preferApiType) {\n            ApiType.Web -> {\n                val response = BiliHttpApi.getVideoMoreInfo(\n                    avid = aid,\n                    cid = cid,\n                    sessData = authRepository.sessionData ?: \"\",\n                    buvid3 = authRepository.buvid3 ?: \"\"\n                ).getResponseData()\n\n                if (response.subtitle == null) {\n                    println(\"get subtitle failed\")\n                } else {\n                    println(\"get subtitle success\")\n                }\n                response.subtitle?.subtitles\n                    ?.map { Subtitle.fromSubtitleItem(it) }\n                    ?: emptyList()\n            }\n\n            ApiType.App -> {\n                val dmViewReply = runCatching {\n                    danmakuStub?.dmView(dmViewReq {\n                        pid = aid.toLong()\n                        oid = cid.toLong()\n                        type = 1\n                    })\n                }.onFailure { handleGrpcException(it) }.getOrThrow()\n                dmViewReply?.subtitle?.subtitlesList\n                    ?.map { Subtitle.fromSubtitleItem(it) }\n                    ?: emptyList()\n            }\n        }\n    }\n\n    suspend fun sendHeartbeat(\n        aid: Long,\n        cid: Long,\n        time: Int,\n        type: HeartbeatVideoType = HeartbeatVideoType.Video,\n        subType: Int? = null,\n        epid: Int? = null,\n        seasonId: Int? = null,\n        preferApiType: ApiType = ApiType.Web\n    ) {\n        val result = when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.sendHeartbeat(\n                avid = aid.toLong(),\n                cid = cid,\n                playedTime = time,\n                type = type.value,\n                subType = subType,\n                epid = epid,\n                sid = seasonId,\n                csrf = authRepository.biliJct,\n                sessData = authRepository.sessionData ?: \"\"\n            )\n\n            ApiType.App -> BiliHttpApi.sendHeartbeat(\n                avid = aid.toLong(),\n                cid = cid,\n                playedTime = time,\n                type = type.value,\n                subType = subType,\n                epid = epid,\n                sid = seasonId,\n                accessKey = authRepository.accessToken ?: \"\"\n            )\n        }\n        println(\"send heartbeat result: $result\")\n    }\n\n    suspend fun getDanmakuMask(\n        aid: Long,\n        cid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): List<DanmakuMaskSegment> {\n        val danmakuMaskUrl = when (preferApiType) {\n            ApiType.Web -> {\n                val response = BiliHttpApi.getVideoMoreInfo(\n                    avid = aid,\n                    cid = cid,\n                    sessData = authRepository.sessionData ?: \"\",\n                    buvid3 = authRepository.buvid3 ?: \"\"\n                ).getResponseData()\n                response.dmMask?.maskUrl\n            }\n\n            ApiType.App -> {\n                val dmViewReply = runCatching {\n                    danmakuStub?.dmView(dmViewReq {\n                        pid = aid\n                        oid = cid\n                        type = 1\n                    })\n                }.onFailure { handleGrpcException(it) }.getOrThrow()\n                dmViewReply?.mask?.maskUrl\n            }\n        } ?: return emptyList()\n\n        val maskBinary = BiliHttpApi.download(danmakuMaskUrl.apply {\n            when (preferApiType) {\n                ApiType.Web -> replace(\"mobmask\", \"webmask\")\n                ApiType.App -> replace(\"webmask\", \"mobmask\")\n            }\n        })\n        val danmakuMaskType = when (preferApiType) {\n            ApiType.Web -> DanmakuMaskType.WebMask\n            ApiType.App -> DanmakuMaskType.MobMask\n        }\n        return DanmakuMask.fromBinary(maskBinary, danmakuMaskType).segments\n    }\n\n    suspend fun getVideoShot(\n        aid: Long,\n        cid: Long,\n        preferApiType: ApiType = ApiType.Web\n    ): VideoShot? {\n        val videoShortResponse = when (preferApiType) {\n            ApiType.Web -> BiliHttpApi.getWebVideoShot(aid = aid, cid = cid)\n            ApiType.App -> BiliHttpApi.getAppVideoShot(aid = aid, cid = cid)\n        }\n        val videoShot = VideoShot.fromVideoShot(videoShortResponse.getResponseData())\n        return videoShot\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/util/AvBvConverter.kt",
    "content": "package dev.aaa1115910.biliapi.util\n\nobject AvBvConverter {\n    private val XOR_CODE = 23442827791579L.toBigInteger()\n    private val MASK_CODE = 2251799813685247L.toBigInteger()\n    private val MAX_AID = 1.toBigInteger() shl 51\n    private val BASE = 58.toBigInteger()\n\n    private const val DATA = \"FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf\"\n\n    fun av2bv(aid: Long): String {\n        val bytes = \"BV1000000000\".toCharArray()\n        var bvIndex = bytes.size - 1\n        var tmp = MAX_AID or aid.toBigInteger() xor XOR_CODE\n        while (tmp > 0.toBigInteger()) {\n            bytes[bvIndex] = DATA[(tmp % BASE).toInt()]\n            tmp /= BASE\n            bvIndex--\n        }\n        bytes.swap(3, 9)\n        bytes.swap(4, 7)\n        return String(bytes)\n    }\n\n    fun bv2av(bvid: String): Long {\n        val bvidArr = bvid.toCharArray()\n        bvidArr.swap(3, 9)\n        bvidArr.swap(4, 7)\n        val adjustedBvid = String(bvidArr, 3, bvidArr.size - 3)\n        var tmp = 0.toBigInteger()\n        for (c in adjustedBvid.toCharArray()) {\n            tmp = tmp * BASE + DATA.indexOf(c).toBigInteger()\n        }\n        val xor = tmp and MASK_CODE xor XOR_CODE\n        return xor.toLong()\n    }\n\n    private fun CharArray.swap(i: Int, j: Int) {\n        this[i] = this[j].also { this[j] = this[i] }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/util/Extends.kt",
    "content": "package dev.aaa1115910.biliapi.util\n\nfun String.convertStringTimeToSeconds(): Int {\n    val parts = this.split(\":\")\n    val hours = if (parts.size == 3) parts[0].toInt() else 0\n    val minutes = parts[parts.size - 2].toInt()\n    val seconds = parts[parts.size - 1].toInt()\n    return (hours * 3600) + (minutes * 60) + seconds\n}\n\nfun Long.toBv(): String = AvBvConverter.av2bv(this)\nfun String.toAv(): Long = AvBvConverter.bv2av(this)"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/util/UrlUtil.kt",
    "content": "package dev.aaa1115910.biliapi.util\n\nimport io.ktor.http.Url\n\nobject UrlUtil {\n    fun isVideoUrl(url: String): Boolean {\n        return url.startsWith(\"bilibili://video/\")\n                || url.startsWith(\"https://www.bilibili.com/video/\")\n    }\n\n    fun parseAidFromUrl(url: String): Long {\n        if (url.startsWith(\"bilibili://video/\")) {\n            return url.split(\"/\").last().toLong()\n        } else {\n            val pathSegments = Url(url).rawSegments\n            val videoSegmentIndex = pathSegments.indexOf(\"video\")\n            val videoId = pathSegments[videoSegmentIndex + 1]\n            return if (videoId.startsWith(\"BV\")) {\n                AvBvConverter.bv2av(videoId)\n            } else {\n                videoId.drop(2).toLong()\n            }\n        }\n    }\n\n    fun parseBvidFromUrl(url: String) = parseAidFromUrl(url).toBv()\n}\n"
  },
  {
    "path": "bili-api/src/main/kotlin/dev/aaa1115910/biliapi/websocket/LiveDataWebSocket.kt",
    "content": "package dev.aaa1115910.biliapi.websocket\n\nimport dev.aaa1115910.biliapi.http.BiliLiveHttpApi\nimport dev.aaa1115910.biliapi.http.entity.live.DanmakuEvent\nimport dev.aaa1115910.biliapi.http.entity.live.FrameHeader\nimport dev.aaa1115910.biliapi.http.entity.live.HostListItem\nimport dev.aaa1115910.biliapi.http.entity.live.LiveEvent\nimport dev.aaa1115910.biliapi.http.entity.live.OnlineRankCountEvent\nimport dev.aaa1115910.biliapi.http.entity.live.PopularityChangeEvent\nimport dev.aaa1115910.biliapi.http.entity.live.readFrameHeader\nimport dev.aaa1115910.biliapi.http.plugins.BiliUserAgent\nimport dev.aaa1115910.biliapi.http.util.BiliDns\nimport dev.aaa1115910.biliapi.http.util.brotliDecompress\nimport dev.aaa1115910.biliapi.http.util.zlibDecompress\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.ktor.client.HttpClient\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.UserAgent\nimport io.ktor.client.plugins.websocket.WebSockets\nimport io.ktor.client.plugins.websocket.wss\nimport io.ktor.utils.io.core.ByteReadPacket\nimport io.ktor.utils.io.core.buildPacket\nimport io.ktor.utils.io.core.remaining\nimport io.ktor.utils.io.core.toByteArray\nimport io.ktor.utils.io.core.writePacket\nimport io.ktor.websocket.Frame\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.util.concurrent.atomic.AtomicInteger\nimport kotlinx.io.readByteArray\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.buildJsonObject\nimport kotlinx.serialization.json.int\nimport kotlinx.serialization.json.jsonArray\nimport kotlinx.serialization.json.jsonObject\nimport kotlinx.serialization.json.jsonPrimitive\nimport kotlinx.serialization.json.long\nimport kotlinx.serialization.json.put\n\nobject LiveDataWebSocket {\n    private lateinit var client: HttpClient\n    private val logger = KotlinLogging.logger { }\n\n    // 预配置的 Json 实例，复用以提高解析效率\n    private val json = Json {\n        ignoreUnknownKeys = true\n        isLenient = true\n    }\n\n    /** 最大重连次数 */\n    private const val MAX_RECONNECT_ATTEMPTS = 5\n    /** 初始重连延迟 (ms) */\n    private const val INITIAL_RECONNECT_DELAY = 1000L\n    /** 最大重连延迟 (ms) */\n    private const val MAX_RECONNECT_DELAY = 30_000L\n\n    private val heartbeat = byteArrayOf(\n        0, 0, 0, 0x1f,\n        0, 0x10, 0, 0x1,\n        0, 0, 0, 0x2,\n        0, 0, 0, 0x1,\n        0x5b, 0x6f, 0x62, 0x6a,\n        0x65, 0x63, 0x74, 0x20,\n        0x4f, 0x62, 0x6a, 0x65,\n        0x63, 0x74, 0x5d\n    )\n\n    init {\n        createClient()\n    }\n\n    private fun createClient() {\n        client = HttpClient(OkHttp) {\n            engine {\n                config {\n                    dns(BiliDns)\n                }\n            }\n            BiliUserAgent()\n            install(WebSockets)\n        }\n    }\n\n    /**\n     * 连接直播事件 WebSocket（自动获取连接信息）\n     * 内部会调用 API 获取 token 和房间号，适用于外部未预取数据的场景\n     */\n    suspend fun connectLiveEvent(\n        roomId: Int,\n        uid: Long = 0,\n        onEvent: (event: LiveEvent) -> Unit\n    ): Job {\n        val danmuInfo =\n            BiliLiveHttpApi.getLiveDanmuInfo(roomId).data ?: throw CancellationException()\n        val realRoomId =\n            BiliLiveHttpApi.getLiveRoomPlayInfo(roomId).data?.roomId\n                ?: throw CancellationException()\n\n        return connectLiveEvent(\n            realRoomId = realRoomId,\n            token = danmuInfo.token,\n            hostList = danmuInfo.hostList,\n            uid = uid,\n            onEvent = onEvent\n        )\n    }\n\n    /**\n     * 连接直播事件 WebSocket（使用预取的连接信息，避免重复 API 调用）\n     * @param realRoomId 真实房间号\n     * @param token 弹幕连接 token\n     * @param hostList WebSocket 主机列表，会按顺序尝试连接\n     * @param uid 用户 uid，已登录用户传入实际 uid，未登录传 0\n     * @param onEvent 事件回调\n     */\n    suspend fun connectLiveEvent(\n        realRoomId: Int,\n        token: String,\n        hostList: List<HostListItem>,\n        uid: Long = 0,\n        onEvent: (event: LiveEvent) -> Unit\n    ): Job {\n        val data = buildJsonObject {\n            put(\"uid\", uid)\n            put(\"roomid\", realRoomId)\n            put(\"protover\", 3)\n            put(\"platform\", \"web\")\n            put(\"type\", 2)\n            put(\"key\", token)\n        }.toString().toByteArray()\n        val authPacket = buildPacket {\n            val size = 16 + data.size\n            writeInt(size) // 封包总大小\n            writeShort(0x10) // 头部大小\n            writeShort(1) // 协议版本\n            writeInt(7) // 类型\n            writeInt(1)\n            writePacket(ByteReadPacket(data))\n        }\n        val authBytes = authPacket.readByteArray()\n\n        val job = client.launch {\n            var reconnectAttempt = 0\n            var reconnectDelay = INITIAL_RECONNECT_DELAY\n\n            while (isActive) {\n                var connected = false\n                // 按顺序尝试 host 列表中的每个服务器\n                for (host in hostList) {\n                    if (!isActive) break\n                    try {\n                        logger.info { \"Connecting to WebSocket host: ${host.host}:${host.wssPort} (attempt ${reconnectAttempt + 1})\" }\n                        client.wss(\n                            host = host.host,\n                            port = host.wssPort,\n                            path = \"/sub\"\n                        ) {\n                            // 连接成功，重置重连计数\n                            connected = true\n                            reconnectAttempt = 0\n                            reconnectDelay = INITIAL_RECONNECT_DELAY\n                            logger.info { \"WebSocket connected to ${host.host}:${host.wssPort}\" }\n\n                            outgoing.send(Frame.Binary(true, authBytes))\n                            launch {\n                                delay(5000)\n                                while (isActive) {\n                                    outgoing.send(Frame.Binary(true, heartbeat))\n                                    delay(30_000)\n                                }\n                            }\n\n                            // 使用固定数量的消费者协程处理消息，避免每条消息创建新协程\n                            val messageChannel = Channel<ByteArray>(Channel.BUFFERED)\n                            val workerCount = 2 // 固定使用2个消费者协程\n                            val activeWorkers = AtomicInteger(workerCount)\n\n                            // 启动固定数量的消费者协程\n                            repeat(workerCount) {\n                                launch {\n                                    try {\n                                        for (eventData in messageChannel) {\n                                            handleLiveEventData(eventData).forEach { event ->\n                                                onEvent(event)\n                                            }\n                                        }\n                                    } finally {\n                                        if (activeWorkers.decrementAndGet() == 0) {\n                                            messageChannel.close()\n                                        }\n                                    }\n                                }\n                            }\n\n                            // 接收消息并发送到 Channel\n                            try {\n                                while (isActive) {\n                                    val frame = incoming.receive()\n                                    val eventData = frame.data\n                                    if (!messageChannel.trySend(eventData).isSuccess) {\n                                        // Channel 满了，丢弃消息（背压处理）\n                                        logger.warn { \"Message channel full, dropping message\" }\n                                    }\n                                }\n                            } finally {\n                                messageChannel.close()\n                            }\n                        }\n                    } catch (e: CancellationException) {\n                        throw e // 不拦截取消\n                    } catch (e: Exception) {\n                        logger.warn { \"WebSocket connection to ${host.host} failed: ${e.message}\" }\n                        // 继续尝试下一个 host\n                    }\n                    if (connected) break\n                }\n\n                // 如果所有 host 都失败了，或者连接断开后需要重连\n                if (!isActive) break\n                reconnectAttempt++\n                if (reconnectAttempt > MAX_RECONNECT_ATTEMPTS) {\n                    logger.error { \"Max reconnect attempts ($MAX_RECONNECT_ATTEMPTS) reached, giving up\" }\n                    break\n                }\n                logger.info { \"Reconnecting in ${reconnectDelay}ms (attempt $reconnectAttempt/$MAX_RECONNECT_ATTEMPTS)\" }\n                delay(reconnectDelay)\n                reconnectDelay = (reconnectDelay * 2).coerceAtMost(MAX_RECONNECT_DELAY)\n            }\n        }\n        job.invokeOnCompletion {\n            logger.info { \"LiveDataWebSocket connection closed: ${it?.message ?: \"normal\"}\" }\n        }\n        return job\n    }\n\n    private suspend fun handleLiveEventData(data: ByteArray): List<LiveEvent> {\n        val result = mutableListOf<LiveEvent>()\n        withContext(Dispatchers.IO) {\n            if (data.size <= 16) return@withContext\n            val bytePack = ByteReadPacket(data)\n            val head = bytePack.readFrameHeader()\n            val body = bytePack.readByteArray((head.totalLength - head.headerLength))\n            result.addAll(handleLiveEventBody(head, body))\n        }\n        return result\n    }\n\n    private fun formatPopularity(popularity: Int): String {\n        return when {\n            popularity >= 100_000_000 -> String.format(\"%.1f亿人气\", popularity / 100_000_000.0)\n            popularity >= 10_000 -> String.format(\"%.1f万人气\", popularity / 10_000.0)\n            else -> \"${popularity}人气\"\n        }\n    }\n\n    private fun handleLiveEventBody(head: FrameHeader, data: ByteArray): List<LiveEvent> {\n        val result = mutableListOf<LiveEvent>()\n        val bytePack = ByteReadPacket(data)\n        when (head.type) {\n            //心跳包回复（人气值）\n            3 -> {\n                // 不从心跳解析人气值，使用 POPULARITY_CHANGE CMD 事件更新\n                runCatching { bytePack.readInt() }\n            }\n\n            //普通包（命令）\n            5 -> {\n                when (head.version.toInt()) {\n                    //0 普通包正文不使用压缩\n                    //1 心跳及认证包正文不使用压缩\n                    0, 1 -> {\n                        val strData = bytePack.readByteArray().decodeToString()\n                        handleLiveCMDEventString(strData)?.let { result += it }\n                    }\n\n                    //普通包正文使用zlib压缩\n                    2 -> {\n                        val decompress = bytePack.readByteArray().zlibDecompress()\n                        result += handleLiveEventBodyDecompress(decompress)\n                    }\n\n                    //普通包正文使用brotli压缩,解压为一个带头部的协议0普通包\n                    3 -> {\n                        val decompress = bytePack.readByteArray().brotliDecompress()\n                        result += handleLiveEventBodyDecompress(decompress)\n                    }\n\n                    else -> {\n                        logger.warn { \"Unknown package version: ${head.version}\" }\n                        bytePack.readByteArray()\n                    }\n                }\n            }\n\n            //认证包回复\n            8 -> {\n                bytePack.readByteArray(10)\n            }\n\n            else -> {\n                logger.warn { \"Unknown package type: ${head.type}\" }\n                bytePack.readByteArray()\n            }\n        }\n        return if (bytePack.remaining > 16) result + handleLiveEventBody(\n            bytePack.readFrameHeader(),\n            bytePack.readByteArray()\n        )\n        else result\n    }\n\n    private fun handleLiveEventBodyDecompress(data: ByteArray): List<LiveEvent> {\n        val result = mutableListOf<LiveEvent>()\n        val bytePack = ByteReadPacket(data)\n        val header = bytePack.readFrameHeader()\n        val body = bytePack.readByteArray(header.dataLength)\n        result += handleLiveCMDEvent(header, body)\n        return if (bytePack.remaining > 0) result + handleLiveEventBodyDecompress(bytePack.readByteArray()) else result\n    }\n\n    private fun handleLiveCMDEvent(head: FrameHeader, data: ByteArray): List<LiveEvent> {\n        val result = mutableListOf<LiveEvent>()\n        val strData: String\n        when (head.version.toInt()) {\n            0 -> {\n                strData = data.decodeToString()\n            }\n\n            2 -> {\n                val decompress = data.zlibDecompress()\n                val bytePack = ByteReadPacket(decompress)\n                val packageHeader = bytePack.readFrameHeader()\n                val body =\n                    bytePack.readByteArray((packageHeader.totalLength - packageHeader.headerLength))\n                if (bytePack.remaining > 16) {\n                    result += handleLiveEventBody(\n                        bytePack.readFrameHeader(),\n                        bytePack.readByteArray()\n                    )\n                }\n                strData = body.decodeToString()\n            }\n\n            else -> {\n                logger.warn { \"Dropped inner packet with unknown version: ${head.version}, dataSize: ${data.size}\" }\n                return result\n            }\n        }\n        handleLiveCMDEventString(strData)?.let { result += it }\n        return result\n    }\n\n    private fun handleLiveCMDEventString(strData: String): LiveEvent? {\n        val dataJson = json.parseToJsonElement(strData).jsonObject\n        val cmd = dataJson[\"cmd\"]!!.jsonPrimitive.content\n\n        when (cmd) {\n            \"COMBO_SEND\" -> {}\n            \"DANMU_MSG\" -> {\n                runCatching {\n                    val infoArray = dataJson[\"info\"]!!.jsonArray\n\n                    // 弹幕内容 info[1]，需要检查是否为字符串（有些情况下是 JSON 对象）\n                    val contentElement = infoArray[1]\n                    if (contentElement !is kotlinx.serialization.json.JsonPrimitive) {\n                        logger.warn { \"DANMU_MSG content is not a string, skipping: $contentElement\" }\n                        return@runCatching\n                    }\n                    val danmakuContent = contentElement.jsonPrimitive.content\n                    \n                    // 弹幕属性 info[0]\n                    val attrArray = infoArray[0].jsonArray\n                    val mode = attrArray[1].jsonPrimitive.int          // 弹幕模式\n                    val fontSize = attrArray[2].jsonPrimitive.int      // 字号\n                    val color = attrArray[3].jsonPrimitive.int         // 颜色\n                    \n                    // 用户信息 info[2]\n                    val userArray = infoArray[2].jsonArray\n                    val senderMid = userArray[0].jsonPrimitive.long\n                    val senderUsername = userArray[1].jsonPrimitive.content\n                    \n                    // 粉丝勋章 info[3]（可能为空数组）\n                    var medalLevel: Int? = null\n                    var medalName: String? = null\n                    runCatching {\n                        val medalArray = infoArray[3].jsonArray\n                        if (medalArray.size > 0) {\n                            medalLevel = medalArray[0].jsonPrimitive.int\n                            medalName = medalArray[1].jsonPrimitive.content\n                        }\n                    }\n\n                    // 用户等级 info[4][0]\n                    var userLevel = 0\n                    runCatching {\n                        val userLevelArray = infoArray[4].jsonArray\n                        if (userLevelArray.size > 0) {\n                            userLevel = userLevelArray[0].jsonPrimitive.int\n                        }\n                    }\n\n                    return DanmakuEvent(\n                        content = danmakuContent,\n                        mid = senderMid,\n                        username = senderUsername,\n                        medalName = medalName,\n                        medalLevel = medalLevel,\n                        mode = mode,\n                        fontSize = fontSize,\n                        color = color,\n                        userLevel = userLevel\n                    )\n                }.onFailure {\n                    logger.warn { \"Parse danmaku content failed: ${it.message}\" }\n                }\n            }\n\n            \"ENTRY_EFFECT\" -> {}\n            //有人上舰\n            \"GUARD_BUY\" -> {}\n            //千舰通知\n            \"GUARD_HONOR_THOUSAND\" -> {\n                println(dataJson)\n            }\n\n            \"HOT_RANK_CHANGED\" -> {}\n            \"HOT_RANK_CHANGED_V2\" -> {}\n            \"HOT_RANK_SETTLEMENT\" -> {}\n            \"HOT_RANK_SETTLEMENT_V2\" -> {}\n            \"HOT_ROOM_NOTIFY\" -> {}\n            \"INTERACT_WORD\" -> {}\n            \"INTERACT_WORD_V2\" -> {}\n            \"LIVE\" -> {\n                logger.info { \"[EVENT-LIVE] $dataJson\" }\n            }\n\n            \"LIVE_INTERACTIVE_GAME\" -> {}\n            \"LIKE_INFO_V3_CLICK\" -> {}\n            \"LIKE_INFO_V3_UPDATE\" -> {}\n            \"LOG_IN_NOTICE\" -> {}\n            \"NOTICE_MSG\" -> {}\n            \"ONLINE_RANK_COUNT\" -> {\n                runCatching {\n                    val data = dataJson[\"data\"]!!.jsonObject\n                    val count = data[\"count\"]!!.jsonPrimitive.int\n                    return OnlineRankCountEvent(count = count)\n                }.onFailure {\n                    logger.warn { \"Parse ONLINE_RANK_COUNT failed: ${it.message}\" }\n                }\n            }\n            \"ONLINE_RANK_V2\" -> {}\n            \"ONLINE_RANK_V3\" -> {}\n            \"ONLINE_RANK_TOP3\" -> {}\n            \"POPULAR_RANK_CHANGED\" -> {\n                logger.info { \"[EVENT-POPULAR_RANK_CHANGED] $dataJson\" }\n            }\n            \"POPULARITY_CHANGE\" -> {\n                logger.info { \"[EVENT-POPULARITY_CHANGE] $dataJson\" }\n                runCatching {\n                    val data = dataJson[\"data\"]!!.jsonObject\n                    val popularity = data[\"popularity\"]!!.jsonPrimitive.int\n                    val popularityText = data[\"popularity_text\"]!!.jsonPrimitive.content\n                    return PopularityChangeEvent(\n                        popularity = popularity,\n                        popularityText = popularityText\n                    )\n                }.onFailure {\n                    logger.warn { \"Parse POPULARITY_CHANGE failed: ${it.message}\" }\n                }\n            }\n            \"PREPARING\" -> {}\n            \"ROOM_REAL_TIME_MESSAGE_UPDATE\" -> {}\n            \"SEND_GIFT\" -> {}\n            \"STOP_LIVE_ROOM_LIST\" -> {}\n            //醒目留言入口提醒（氪金提醒）\n            \"SUPER_CHAT_ENTRANCE\" -> {}\n            //醒目留言\n            \"SUPER_CHAT_MESSAGE\" -> {}\n            //醒目留言\n            \"SUPER_CHAT_MESSAGE_JPN\" -> {}\n            \"SYS_MSG\" -> {\n                println(dataJson)\n            }\n\n            \"USER_TOAST_MSG\" -> {}\n            \"WATCHED_CHANGE\" -> {}\n            \"WIDGET_BANNER\" -> {}\n            else -> {\n                logger.warn { \"Unknown live event: $cmd\" }\n                logger.warn { dataJson }\n            }\n        }\n        return null\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/BvLoginRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi\n\nimport dev.aaa1115910.biliapi.http.BiliPassportHttpApi\nimport dev.aaa1115910.biliapi.http.util.generateBuvid\nimport dev.aaa1115910.biliapi.repositories.LoginRepository\nimport dev.aaa1115910.biliapi.repositories.SendSmsResult\nimport dev.aaa1115910.biliapi.repositories.SendSmsState\nimport kotlinx.coroutines.runBlocking\nimport java.net.URL\n\nclass BvLoginRepositoryTest {\n    private val loginRepository = LoginRepository()\n    private val phone = 16215705468L\n    private val buvid = generateBuvid()\n    private val loginSessionId = loginRepository.generateLoginSessionId()\n    var captchaKey: String? = null\n\n    fun `send sms`() = runBlocking {\n\n        println(\n            \"\"\"\n            buvid: $buvid\n            loginSessionId: $loginSessionId\n            phoneNumber: $phone\n        \"\"\".trimIndent()\n        )\n\n        var recaptchaToken: String? = null\n        var geetestChallenge: String? = null\n        var geetestValidate: String? = null\n        var geetestGt: String? = null\n\n        var sendSmsResult: SendSmsResult? = null\n        runCatching {\n            sendSmsResult = loginRepository.requestSms(\n                phone = phone,\n                loginSessionId = loginSessionId,\n                buvid = buvid\n            )\n            println(\"Request send sms result: $sendSmsResult\")\n        }.onFailure {\n            it.printStackTrace()\n            return@runBlocking\n        }\n\n        when (sendSmsResult!!.state) {\n            SendSmsState.Error -> {\n                println(\"error\")\n            }\n\n            SendSmsState.Success -> {\n                println(\"send sms success\")\n                captchaKey = sendSmsResult!!.captchaKey\n                println(\"captcha key: $captchaKey\")\n            }\n\n            SendSmsState.RecaptchaRequire -> {\n                println(\"require recaptcha\")\n                println(\"recaptcha url: ${sendSmsResult!!.recaptchaUrl}\")\n\n                URL(sendSmsResult!!.recaptchaUrl).query.split(\"&\").forEach {\n                    val (key, value) = it.split(\"=\")\n                    when (key) {\n                        \"recaptcha_token\" -> recaptchaToken = value\n                        \"gee_gt\" -> geetestGt = value\n                        \"gee_challenge\" -> geetestChallenge = value\n                    }\n                }\n\n                println(\n                    \"\"\"\n                    recaptchaToken: $recaptchaToken\n                    geetestGt: $geetestGt\n                    geetestChallenge: $geetestChallenge\n                \"\"\".trimIndent()\n                )\n                print(\"Please input the validate: \")\n                geetestValidate = readln()\n                //应使用新的 challenge\n                print(\"Please input the challenge: \")\n                geetestChallenge = readln()\n\n                //retry send sms\n                runCatching {\n                    sendSmsResult = loginRepository.requestSms(\n                        phone = phone,\n                        loginSessionId = loginSessionId,\n                        buvid = buvid,\n                        recaptchaToken = recaptchaToken,\n                        geetestChallenge = \"$geetestChallenge\",\n                        geetestValidate = geetestValidate,\n                    )\n                    println(\"Request send sms result: $sendSmsResult\")\n                }.onFailure {\n                    it.printStackTrace()\n                    return@runBlocking\n                }\n\n                when (sendSmsResult!!.state) {\n                    SendSmsState.Success -> {\n                        println(\"send sms success\")\n                        captchaKey = sendSmsResult!!.captchaKey\n                        println(\"captcha key: $captchaKey\")\n                    }\n\n                    else -> {\n                        println(\"error\")\n                    }\n                }\n            }\n\n            else -> {}\n        }\n    }\n\n    fun `login with sms`() = runBlocking {\n        println(\"====login with sms==== \")\n        print(\"please input the sms code: \")\n        val code = readln().toInt()\n        val response = BiliPassportHttpApi.loginWithSms(\n            cid = 86,\n            tel = phone,\n            loginSessionId = loginSessionId,\n            code = code,\n            captchaKey = captchaKey!!\n        )\n        println(response.getResponseData())\n    }\n}\n\nfun main() {\n    val test = BvLoginRepositoryTest()\n    test.`send sms`()\n    test.captchaKey?.let {\n        test.`login with sms`()\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/entity/DanmakuMaskTest.kt",
    "content": "package dev.aaa1115910.biliapi.entity\n\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMask\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskType\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMobMaskFrame\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuWebMaskFrame\nimport java.io.File\nimport kotlin.test.Test\n\nclass DanmakuMaskTest {\n    @Test\n    fun `parse web mask file`() {\n        val maskFile = Any::class::class.java.getResource(\"/3540266_25_2.exp.webmask\")!!\n        val binary = File(maskFile.toURI()).readBytes()\n        val mask = DanmakuMask.fromBinary(binary, DanmakuMaskType.WebMask)\n        println(mask)\n    }\n\n    @Test\n    fun `parse web mask file and output`() {\n        val maskFile = Any::class::class.java.getResource(\"/3540266_25_2.exp.webmask\")!!\n        val binary = File(maskFile.toURI()).readBytes()\n        val mask = DanmakuMask.fromBinary(binary, DanmakuMaskType.WebMask)\n        val outputDir = File(\"/home/seele/Documents/output/webmask\")\n        outputDir.mkdirs()\n        mask.segments.forEachIndexed { index, danmakuMaskSegment ->\n            val dir = File(outputDir, \"$index\")\n            dir.mkdir()\n            danmakuMaskSegment.frames.forEach { danmakuMaskFrame ->\n                File(dir, \"${danmakuMaskFrame.range}.svg\")\n                    .writeText((danmakuMaskFrame as DanmakuWebMaskFrame).svg)\n            }\n        }\n    }\n\n    @Test\n    fun `parse mob mask file`() {\n        val maskFile = Any::class::class.java.getResource(\"/3540266_25_2.exp.mobmask\")!!\n        val binary = File(maskFile.toURI()).readBytes()\n        val mask = DanmakuMask.fromBinary(binary, DanmakuMaskType.MobMask)\n        println()\n    }\n\n    @OptIn(ExperimentalStdlibApi::class)\n    @Test\n    fun `parse mob mask file and output`() {\n        val maskFile = Any::class::class.java.getResource(\"/3540266_25_2.exp.mobmask\")!!\n        val binary = File(maskFile.toURI()).readBytes()\n        val mask = DanmakuMask.fromBinary(binary, DanmakuMaskType.MobMask)\n        val outputDir = File(\"/home/seele/Documents/output/mobmask\")\n        outputDir.mkdirs()\n        mask.segments.forEachIndexed { index, danmakuMaskSegment ->\n            val dir = File(outputDir, \"$index\")\n            dir.mkdir()\n            danmakuMaskSegment.frames.forEach { danmakuMaskFrame ->\n                val content = (danmakuMaskFrame as DanmakuMobMaskFrame)\n                    .image\n                    .toHexString(HexFormat.Default)\n                    .chunked(80)\n                    .joinToString(\"\\n\")\n                File(dir, \"${danmakuMaskFrame.range}.txt\").writeText(content)\n            }\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/http/BiliHttpApiTest.kt",
    "content": "package dev.aaa1115910.biliapi.http\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonStatus\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonType\nimport dev.aaa1115910.biliapi.http.entity.user.FollowAction\nimport dev.aaa1115910.biliapi.http.entity.user.FollowActionSource\nimport dev.aaa1115910.biliapi.http.util.generateBuvid\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Assertions.assertDoesNotThrow\nimport org.junit.jupiter.api.Test\nimport java.io.File\nimport java.nio.file.Paths\nimport java.util.Properties\n\ninternal class BiliHttpApiTest {\n\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n    }\n\n    @Test\n    fun `println sessdata and bili_jct`() {\n        println(\"SESSDATA: $SESSDATA\")\n        println(\"BILI_JCT: $BILI_JCT\")\n    }\n\n    @Test\n    fun `get popular videos`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getPopularVideoData()\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get video info`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getVideoInfo(av = 170001)\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get video info which is ugc season`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getVideoInfo(av = 433139956)\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get video play url`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getVideoPlayUrl(\n                    av = 648092492,\n                    cid = 903675075,\n                    fnval = 4048,\n                    qn = 127,\n                    sessData = SESSDATA,\n                    dedeUserID = UID\n                )\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get pgc video play url`() {\n        runBlocking {\n            println(\n                BiliHttpApi.getPgcVideoPlayUrl(\n                    av = 672676070,\n                    cid = 331748015,\n                    fnval = 4048,\n                    qn = 127,\n                    sessData = SESSDATA,\n                    dedeUserID = UID\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `get pgc video play url v2`() {\n        runBlocking {\n            println(\n                BiliHttpApi.getPgcVideoPlayUrlV2(\n                    av = 672676070,\n                    cid = 331748015,\n                    fnval = 4048,\n                    qn = 127,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `get video danmaku from xml`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getDanmakuXml(cid = 903675075)\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get dynamic list with type all`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getDynamicList(\n                    type = \"article\",\n                    sessData = SESSDATA\n                )\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get user info from Mr_He`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getUserInfo(\n                    uid = 163637592,\n                    sessData = SESSDATA\n                )\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get user card info from Mr_He`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getUserCardInfo(\n                    uid = 163637592,\n                    photo = true,\n                    sessData = SESSDATA\n                )\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get self user info`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getUserSelfInfo(\n                    sessData = SESSDATA\n                )\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get histories`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getHistories(\n                    viewAt = 0,\n                    sessData = SESSDATA\n                )\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get related vidoes`() {\n        assertDoesNotThrow {\n            runBlocking {\n                val response = BiliHttpApi.getRelatedVideos(\n                    avid = 170001\n                )\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get favorite folder metadata from id 2333`() {\n        runBlocking {\n            val response = BiliHttpApi.getFavoriteFolderInfo(\n                mediaId = 2333\n            )\n            println(response)\n        }\n    }\n\n    @Test\n    fun `get all favorite folders metadata`() {\n        runBlocking {\n            val response = BiliHttpApi.getAllFavoriteFoldersInfo(\n                mid = 2333,\n                sessData = SESSDATA\n            )\n            println(response)\n        }\n    }\n\n    @Test\n    fun `get all favorite item ids`() {\n        runBlocking {\n            val response = BiliHttpApi.getFavoriteIdList(\n                mediaId = 2333,\n                sessData = SESSDATA\n            )\n            println(response)\n        }\n    }\n\n    @Test\n    fun `get favorite list`() {\n        runBlocking {\n            val response = BiliHttpApi.getFavoriteList(\n                mediaId = 2333,\n                sessData = SESSDATA\n            )\n            println(response)\n        }\n    }\n\n    @Test\n    fun `send heartbeat`() {\n        runBlocking {\n            val response = BiliHttpApi.sendHeartbeat(\n                avid = 170001,\n                cid = 280468,\n                playedTime = 23,\n                sessData = SESSDATA\n            )\n            println(response)\n        }\n    }\n\n    @Test\n    fun `get video more info`() {\n        runBlocking {\n            val response = BiliHttpApi.getVideoMoreInfo(\n                avid = 170001,\n                cid = 279786,\n                sessData = SESSDATA,\n                buvid3 = generateBuvid()\n            ).getResponseData()\n            println(\"lastPlayTime: ${response.lastPlayTime}\")\n            println(\"lastPlayCid: ${response.lastPlayCid}\")\n        }\n    }\n\n    @Test\n    fun `send video like`() {\n        runBlocking {\n            println(\n                BiliHttpApi.sendVideoLike(\n                    avid = 170001,\n                    like = true,\n                    csrf = BILI_JCT,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `check video is liked`() {\n        runBlocking {\n            println(\n                BiliHttpApi.checkVideoLiked(\n                    avid = 170001,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `send video coin`() {\n        runBlocking {\n            println(\n                BiliHttpApi.sendVideoCoin(\n                    avid = 170001,\n                    csrf = BILI_JCT,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `check video coin`() {\n        runBlocking {\n            println(\n                BiliHttpApi.checkVideoSentCoin(\n                    avid = 170001,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `add video to favorite`() {\n        runBlocking {\n            println(\n                BiliHttpApi.setVideoToFavorite(\n                    avid = 170001,\n                    addMediaIds = listOf(46912037),\n                    csrf = BILI_JCT,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `delete video from favorite`() {\n        runBlocking {\n            println(\n                BiliHttpApi.setVideoToFavorite(\n                    avid = 170001,\n                    delMediaIds = listOf(46912037),\n                    csrf = BILI_JCT,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `check is video in favorite`() {\n        runBlocking {\n            println(\n                BiliHttpApi.checkVideoFavoured(\n                    avid = 170001,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `get user space videos`() = runBlocking {\n        println(\n            BiliHttpApi.getWebUserSpaceVideos(\n                mid = 1,\n                sessData = SESSDATA\n            )\n        )\n    }\n\n    @Test\n    fun `get web season info data`() {\n        runBlocking {\n            println(\n                BiliHttpApi.getWebSeasonInfo(\n                    epId = 705917\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `get app season info data`() = runBlocking {\n        println(\n            BiliHttpApi.getAppSeasonInfo(\n                epId = 752900,\n                seasonId = 45303,\n                mobiApp = \"android_hd\",\n                accessKey = ACCESS_TOKEN\n            )\n        )\n    }\n\n    @Test\n    fun `get user season status data`() = runBlocking {\n        println(\n            BiliHttpApi.getSeasonUserStatus(\n                seasonId = 44152,\n                sessData = SESSDATA\n            )\n        )\n    }\n\n    @Test\n    fun `get video tags`() {\n        runBlocking {\n            println(\n                BiliHttpApi.getVideoTags(\n                    avid = 170001\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `get tag detail`() = runBlocking {\n        println(\n            BiliHttpApi.getTagDetail(\n                tagId = 6020278,\n                pageNumber = 1,\n                pageSize = 20\n            )\n        )\n    }\n\n    @Test\n    fun `get tag popular videos`() = runBlocking {\n        println(\n            BiliHttpApi.getTagTopVideos(\n                tagId = 6020278,\n                pageNumber = 1,\n                pageSize = 20\n            )\n        )\n    }\n\n    @Test\n    fun `get web timeline`() = runBlocking {\n        val result = BiliHttpApi.getTimeline(\n            type = 1,\n            before = 7,\n            after = 7\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get app timeline`() = runBlocking {\n        val result = BiliHttpApi.getTimeline(\n            filterType = 0\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get follow list`() {\n        runBlocking {\n            println(\n                BiliHttpApi.getUserFollow(\n                    mid = 3066511,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `add follow`() {\n        runBlocking {\n            println(\n                BiliHttpApi.modifyFollow(\n                    mid = 3066511,\n                    action = FollowAction.AddFollow,\n                    actionSource = FollowActionSource.Space,\n                    csrf = BILI_JCT,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `delete follow`() {\n        runBlocking {\n            println(\n                BiliHttpApi.modifyFollow(\n                    mid = 3066511,\n                    action = FollowAction.DelFollow,\n                    actionSource = FollowActionSource.Space,\n                    csrf = BILI_JCT,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `get user relations`() {\n        runBlocking {\n            println(\n                BiliHttpApi.getRelations(\n                    mid = 11336264,\n                    sessData = SESSDATA\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `get user relation stat`() {\n        runBlocking {\n            println(\n                BiliHttpApi.getRelationStat(\n                    mid = 11336264\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `get web search hot words`() = runBlocking {\n        println(BiliHttpApi.getWebSearchSquare())\n    }\n\n    @Test\n    fun `get app search hot words`() = runBlocking {\n        println(BiliHttpApi.getAppSearchSquare())\n    }\n\n    @Test\n    fun `get app search trending ranking`() = runBlocking {\n        println(BiliHttpApi.getSearchTrendRank())\n    }\n\n    @Test\n    fun `get search keyword suggests`() {\n        runBlocking {\n            println(\n                BiliHttpApi.getKeywordSuggest(\n                    term = \"和奥托一起泡温泉\",\n                    buvid = BUVID\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `search all`() {\n        runBlocking {\n            println(\n                BiliHttpApi.searchAll(\n                    keyword = \"007\"\n                )\n            )\n        }\n    }\n\n    @Test\n    fun `search type`() {\n        val types =\n            listOf(\"video\", \"media_bangumi\", \"media_ft\", \"article\", \"topic\", \"bili_user\")\n        runBlocking {\n            types.forEach { type ->\n                println(\n                    BiliHttpApi.searchType(\n                        keyword = \"007\",\n                        type = type\n                    )\n                )\n            }\n        }\n    }\n\n    @Test\n    fun `get web initial state data`() {\n        runBlocking {\n            PgcType.entries.forEach { pgcType ->\n                println(\"type: ${pgcType.name}\")\n                println(\n                    BiliHttpApi.getPgcWebInitialStateData(pgcType)\n                        .toString().replace(\"\\n\", \"\")\n                )\n            }\n        }\n    }\n\n    @Test\n    fun `get pgc feed data`() {\n        runBlocking {\n            PgcType.entries.forEach { pgcType ->\n                println(\"type: ${pgcType.name}\")\n                when (pgcType) {\n                    PgcType.Anime, PgcType.GuoChuang ->\n                        println(\n                            BiliHttpApi.getPgcFeedV3(name = pgcType.name.lowercase())\n                                .toString().replace(\"\\n\", \"\")\n                        )\n\n                    PgcType.Tv, PgcType.Movie, PgcType.Documentary, PgcType.Variety ->\n                        println(\n                            BiliHttpApi.getPgcFeed(name = pgcType.name.lowercase())\n                                .toString().replace(\"\\n\", \"\")\n                        )\n                }\n            }\n        }\n    }\n\n    @Test\n    fun `get web following season data`() {\n        runBlocking {\n            for (followingSeasonType in FollowingSeasonType.values()) {\n                for (followingSeasonStatus in FollowingSeasonStatus.values()) {\n                    println(\"type: $followingSeasonType, status: $followingSeasonStatus: \")\n                    println(\n                        BiliHttpApi.getFollowingSeasons(\n                            type = followingSeasonType.id,\n                            status = followingSeasonStatus.id,\n                            pageNumber = 1,\n                            pageSize = 1,\n                            mid = UID,\n                            sessData = SESSDATA\n                        )\n                    )\n                }\n            }\n        }\n    }\n\n    @Test\n    fun `get app following season data`() {\n        runBlocking {\n            for (followingSeasonType in FollowingSeasonType.values()) {\n                for (followingSeasonStatus in FollowingSeasonStatus.values()) {\n                    println(\"type: $followingSeasonType, status: $followingSeasonStatus: \")\n                    println(\n                        BiliHttpApi.getFollowingSeasons(\n                            type = followingSeasonType.paramName,\n                            status = followingSeasonStatus.id,\n                            pageNumber = 1,\n                            pageSize = 1,\n                            build = 6830300,\n                            accessKey = ACCESS_TOKEN\n                        )\n                    )\n                }\n            }\n        }\n    }\n\n    @Test\n    fun `get web homepage recommend items`() = runBlocking {\n        val result = BiliHttpApi.getFeedRcmd()\n        println(result)\n    }\n\n    @Test\n    fun `get app homepage recommend items`() = runBlocking {\n        val result = BiliHttpApi.getFeedIndex()\n        println(result)\n    }\n\n    @Test\n    fun `get anime index`() = runBlocking {\n        val result = BiliHttpApi.seasonIndexAnimeResult()\n        println(result.data?.list?.map { it.title })\n    }\n\n    @Test\n    fun `get guochuang index`() = runBlocking {\n        val result = BiliHttpApi.seasonIndexGuochuangResult()\n        println(result.data?.list?.map { it.title })\n    }\n\n    @Test\n    fun `get movie index`() = runBlocking {\n        val result = BiliHttpApi.seasonIndexMovieResult()\n        println(result.data?.list?.map { it.title })\n    }\n\n    @Test\n    fun `get tv index`() = runBlocking {\n        val result = BiliHttpApi.seasonIndexTvResult()\n        println(result.data?.list?.map { it.title })\n    }\n\n    @Test\n    fun `get variety season index`() = runBlocking {\n        val result = BiliHttpApi.seasonIndexVarietyResult()\n        println(result.data?.list?.map { it.title })\n    }\n\n    @Test\n    fun `get documentary season index`() = runBlocking {\n        val result = BiliHttpApi.seasonIndexDocumentaryResult()\n        println(result.data?.list?.map { it.title })\n    }\n\n    @Test\n    fun `get web video shot`() = runBlocking {\n        val result = BiliHttpApi.getWebVideoShot(aid = 170001)\n        println(result)\n    }\n\n    @Test\n    fun `get app video shot`() = runBlocking {\n        val result = BiliHttpApi.getAppVideoShot(aid = 170001, cid = 279786)\n        println(result)\n    }\n\n    @Test\n    fun `get app region dynamic`() = runBlocking {\n        val rids = listOf(\n            1, 13, 167, 3, 129, 4, 36, 188, 234, 223, 160,\n            211, 217, 119, 155, 202, 5, 181, 177, 23, 11\n        )\n        rids\n            .shuffled()\n            .forEach { rid ->\n                println(\"rid $rid:\")\n                val result = BiliHttpApi.getRegionDynamic(\n                    rid = rid,\n                    accessKey = ACCESS_TOKEN\n                )\n                println(result)\n                delay((800L..2000L).random())\n            }\n    }\n\n\n    @Test\n    fun `get app region dynamic list`() = runBlocking {\n        val rids = listOf(\n            1, 13, 167, 3, 129, 4, 36, 188, 234, 223, 160,\n            211, 217, 119, 155, 202, 5, 181, 177, 23, 11\n        )\n        rids\n            .shuffled()\n            .forEach { rid ->\n                println(\"rid $rid:\")\n                val result = BiliHttpApi.getRegionDynamicList(\n                    rid = rid,\n                    accessKey = ACCESS_TOKEN\n                )\n                println(result)\n                delay((800L..2000L).random())\n            }\n    }\n\n    @Test\n    fun `get locs`() = runBlocking {\n        val locIds = listOf(\n            4973, 4991, 5004, 4979, 4985, 5008, 5007, 4997,\n            4998, 5005, 5002, 5001, 5000, 5006, 4999, 5003\n        )\n\n        locIds.chunked(3).forEach { locs ->\n            println(\"${locs.joinToString(\",\")}:\")\n            val result = BiliHttpApi.getLocs(\n                ids = locs\n            )\n            println(result)\n            delay((800L..2000L).random())\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/http/BiliLiveHttpApiTest.kt",
    "content": "package dev.aaa1115910.biliapi.http\n\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Assertions\nimport org.junit.jupiter.api.Test\n\nclass BiliLiveHttpApiTest {\n    @Test\n    fun `get history live room danmaku`() {\n        Assertions.assertDoesNotThrow {\n            runBlocking {\n                val response = BiliLiveHttpApi.getLiveDanmuHistory(roomId = 22739471)\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get live event websocket connect url and token`() {\n        Assertions.assertDoesNotThrow {\n            runBlocking {\n                val response = BiliLiveHttpApi.getLiveDanmuInfo(roomId = 22739471)\n                println(response)\n            }\n        }\n    }\n\n    @Test\n    fun `get live room info`() {\n        Assertions.assertDoesNotThrow {\n            runBlocking {\n                val response = BiliLiveHttpApi.getLiveRoomPlayInfo(roomId = 22739471)\n                println(response)\n            }\n        }\n    }\n\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/http/BiliPassportHttpApiTest.kt",
    "content": "package dev.aaa1115910.biliapi.http\n\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\nimport kotlin.test.assertEquals\n\nclass BiliPassportHttpApiTest {\n    @Test\n    fun `get web qr login url`() {\n        val response = runBlocking { BiliPassportHttpApi.getWebQRUrl() }\n        println(\"qr url: ${response.data?.url}\")\n        println(\"qr key: ${response.data?.qrcodeKey}\")\n        assertEquals(0, response.code)\n    }\n\n    @Test\n    fun `request web qr login result`() {\n        val qrUrlResponse = runBlocking { BiliPassportHttpApi.getWebQRUrl() }\n        val url = qrUrlResponse.data?.url\n        val key = qrUrlResponse.data?.qrcodeKey\n        println(\"qr url: $url\")\n        println(\"qr key: $key\")\n        var loop = true\n        while (loop) {\n            val (loginResponse, cookies) = runBlocking { BiliPassportHttpApi.loginWithWebQR(key!!) }\n            when (val result = loginResponse.data?.code) {\n                0 -> {\n                    loop = false\n                    println(\"login success\")\n                }\n\n                86101 -> println(\"wait to scan\")\n                86090 -> println(\"wait to confirm\")\n                86038 -> {\n                    loop = false\n                    println(\"qr expired\")\n                }\n\n                else -> {\n                    loop = false\n                    println(\"unknown code: $result\")\n                }\n            }\n            if (loop) {\n                runBlocking { delay(1000) }\n            } else {\n                println(loginResponse)\n                println(cookies)\n            }\n        }\n    }\n\n    @Test\n    fun `get app qr login url`() {\n        val response = runBlocking {\n            BiliPassportHttpApi.getAppQRUrl(\n                localId = \"1\",\n                ts = (System.currentTimeMillis() / 1000).toInt(),\n                mobiApp = \"android_hd\"\n            )\n        }\n        println(response)\n        println(\"qr url: ${response.data?.url}\")\n        println(\"qr key: ${response.data?.authCode}\")\n        assertEquals(0, response.code)\n    }\n\n    @Test\n    fun `request app qr login result`() {\n        val qrUrlResponse = runBlocking {\n            BiliPassportHttpApi.getAppQRUrl(\n                localId = \"1\",\n                ts = (System.currentTimeMillis() / 1000).toInt(),\n                mobiApp = \"android_hd\"\n            )\n        }\n        val url = qrUrlResponse.data?.url\n        val key = qrUrlResponse.data?.authCode\n        println(\"qr url: $url\")\n        println(\"qr key: $key\")\n        var loop = true\n        while (loop) {\n            val loginResponse = runBlocking {\n                BiliPassportHttpApi.loginWithAppQR(\n                    authCode = key!!,\n                    localId = \"1\",\n                    ts = (System.currentTimeMillis() / 1000).toInt()\n                )\n            }\n            println(loginResponse)\n            when (val result = loginResponse.code) {\n                0 -> {\n                    loop = false\n                    println(\"login success\")\n                }\n\n                86039 -> println(\"wait to scan\")\n                86090 -> println(\"wait to confirm\")\n                86038 -> {\n                    loop = false\n                    println(\"qr expired\")\n                }\n\n                else -> {\n                    loop = false\n                    println(\"unknown code: $result\")\n                }\n            }\n            if (loop) {\n                runBlocking { delay(1000) }\n            } else {\n                println(loginResponse)\n            }\n        }\n    }\n\n    @Test\n    fun `get captcha`() = runBlocking {\n        println(BiliPassportHttpApi.getCaptcha())\n    }\n\n    // this is a random phone number\n    val tel = 13300000001L\n    val loginSessionId = \"144525a8fd2811edbe560242ac120002\"\n    val recaptchaToken = \"766877c778b2425eb2c6fbc4791e2b43\"\n    val geeChallenge = \"5ccbb4431fa57f5476e65facd9f2b111\"\n    val geeValidate = \"04721095469a0a08c3f124a71c7b3c49\"\n    val buvid = \"XYaa24a8d76a1140a332c16e1e2d4d66318ff\"\n\n    @Test\n    fun `send sms`() = runBlocking {\n        println(\n            BiliPassportHttpApi.sendSms(\n                cid = 86,\n                tel = tel,\n                loginSessionId = loginSessionId,\n                channel = \"bili\",\n                buvid = buvid,\n                statistics = \"\"\"{\"appId\":1,\"platform\":3,\"version\":\"7.27.0\",\"abtest\":\"\"}\"\"\",\n                ts = System.currentTimeMillis() / 1000\n            )\n        )\n    }\n\n    @Test\n    fun `send sms with captcha`() = runBlocking {\n        println(\n            BiliPassportHttpApi.sendSms(\n                cid = 86,\n                tel = tel,\n                loginSessionId = loginSessionId,\n                recaptchaToken = recaptchaToken,\n                geeChallenge = geeChallenge,\n                geeValidate = geeValidate,\n                geeSeccode = \"$geeValidate|jordan\",\n                channel = \"bili\",\n                buvid = buvid,\n                statistics = \"\"\"{\"appId\":1,\"platform\":3,\"version\":\"7.27.0\",\"abtest\":\"\"}\"\"\",\n                ts = System.currentTimeMillis() / 1000\n            )\n        )\n    }\n\n    @Test\n    fun `login with sms`() = runBlocking {\n        println(\n            BiliPassportHttpApi.loginWithSms(\n                cid = 86,\n                tel = tel,\n                loginSessionId = loginSessionId,\n                code = 23,\n                captchaKey = \"\"\n            )\n        )\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/http/BiliPlusHttpApiTest.kt",
    "content": "package dev.aaa1115910.biliapi.http\n\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\n\nclass BiliPlusHttpApiTest {\n\n    @Test\n    fun `get season video view`() = runBlocking {\n        val result = BiliPlusHttpApi.view(955635143)\n        println(result)\n    }\n\n    @Test\n    fun `get normal video view`() = runBlocking {\n        val result = BiliPlusHttpApi.view(955635143)\n        println(result)\n    }\n\n    @Test\n    fun `get not found video view`() = runBlocking {\n        val result = BiliPlusHttpApi.view(1)\n        println(result)\n    }\n\n    @Test\n    fun `get season id by avid`() = runBlocking {\n        val seasonId = BiliPlusHttpApi.getSeasonIdByAvid(314583081)\n        println(seasonId)\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/CommentRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport kotlinx.coroutines.runBlocking\nimport java.io.File\nimport java.nio.file.Paths\nimport java.util.Properties\nimport kotlin.test.Test\n\nclass CommentRepositoryTest {\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n    }\n\n    private val authRepository = AuthRepository()\n    private val channelRepository = ChannelRepository()\n    private val commentRepository = CommentRepository(authRepository, channelRepository)\n\n    init {\n        channelRepository.initDefaultChannel(\n            FavoriteRepositoryTest.ACCESS_TOKEN,\n            FavoriteRepositoryTest.BUVID\n        )\n\n        authRepository.sessionData = FavoriteRepositoryTest.SESSDATA\n        authRepository.accessToken = FavoriteRepositoryTest.ACCESS_TOKEN\n        authRepository.biliJct = FavoriteRepositoryTest.BILI_JCT\n    }\n\n    @Test\n    fun `get comments`() = runBlocking {\n        val commentId = 1018604077579239458L\n        val commentType = 17L\n        ApiType.entries.forEach { apiType ->\n            val commentsData = commentRepository.getComments(\n                id = commentId,\n                type = commentType,\n                preferApiType = apiType\n            )\n            println(\"ApiType: $apiType\")\n            println(commentsData)\n        }\n    }\n\n    @Test\n    fun `get comment replies`() = runBlocking {\n        val commentId = 1018604077579239458L\n        val commentType = 17L\n        val rpid = 251515293904\n        ApiType.entries.forEach { apiType ->\n            val commentsData = commentRepository.getCommentReplies(\n                rpid = rpid,\n                commentId = commentId,\n                type = commentType,\n                preferApiType = apiType\n            )\n            println(\"ApiType: $apiType\")\n            println(commentsData)\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/FavoriteRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\nimport java.io.File\nimport java.nio.file.Paths\nimport java.util.Properties\n\nclass FavoriteRepositoryTest {\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n    }\n\n    private val authRepository = AuthRepository()\n    private val channelRepository = ChannelRepository()\n    private val favoriteRepository = FavoriteRepository(authRepository)\n\n    init {\n        channelRepository.initDefaultChannel(ACCESS_TOKEN, BUVID)\n\n        authRepository.sessionData = SESSDATA\n        authRepository.accessToken = ACCESS_TOKEN\n        authRepository.biliJct = BILI_JCT\n    }\n\n    @Test\n    fun `check video is favoured with cookies`() = runBlocking {\n        val result = favoriteRepository.checkVideoFavoured(\n            aid = 170001,\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `check video is favoured with token`() = runBlocking {\n        val result = favoriteRepository.checkVideoFavoured(\n            aid = 170001,\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `add video to favorite folder with cookies`() = runBlocking {\n        val defaultMediaId = getDefaultFavoriteFolderId(ApiType.Web)\n        favoriteRepository.addVideoToFavoriteFolder(\n            aid = 170001,\n            addMediaIds = listOf(defaultMediaId),\n            preferApiType = ApiType.Web\n        )\n    }\n\n    @Test\n    fun `add video to favorite folder with token`() = runBlocking {\n        val defaultMediaId = getDefaultFavoriteFolderId(ApiType.App)\n        favoriteRepository.addVideoToFavoriteFolder(\n            aid = 170001,\n            addMediaIds = listOf(defaultMediaId),\n            preferApiType = ApiType.App\n        )\n    }\n\n    @Test\n    fun `del video from favorite folder with cookies`() = runBlocking {\n        val defaultMediaId = getDefaultFavoriteFolderId(ApiType.Web)\n        favoriteRepository.delVideoFromFavoriteFolder(\n            aid = 170001,\n            delMediaIds = listOf(defaultMediaId),\n            preferApiType = ApiType.Web\n        )\n    }\n\n    @Test\n    fun `del video from favorite folder with token`() = runBlocking {\n        val defaultMediaId = getDefaultFavoriteFolderId(ApiType.App)\n        favoriteRepository.delVideoFromFavoriteFolder(\n            aid = 170001,\n            delMediaIds = listOf(defaultMediaId),\n            preferApiType = ApiType.App\n        )\n    }\n\n    @Test\n    fun `update video to favorite folder with cookies`() = runBlocking {\n        val defaultMediaId = getDefaultFavoriteFolderId(ApiType.App)\n        favoriteRepository.updateVideoToFavoriteFolder(\n            aid = 170001,\n            addMediaIds = listOf(defaultMediaId),\n            delMediaIds = listOf(),\n            preferApiType = ApiType.Web\n        )\n    }\n\n    @Test\n    fun `update video to favorite folder with token`() = runBlocking {\n        val defaultMediaId = getDefaultFavoriteFolderId(ApiType.App)\n        favoriteRepository.updateVideoToFavoriteFolder(\n            aid = 170001,\n            addMediaIds = listOf(defaultMediaId),\n            delMediaIds = listOf(),\n            preferApiType = ApiType.App\n        )\n    }\n\n    @Test\n    fun `get all favorite folders metadata with cookies`() = runBlocking {\n        val result = favoriteRepository.getAllFavoriteFolderMetadataList(\n            mid = UID,\n            rid = 170001,\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get all favorite folders metadata with token`() = runBlocking {\n        val result = favoriteRepository.getAllFavoriteFolderMetadataList(\n            mid = UID,\n            rid = 170001,\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get favorite folder data with cookies`() = runBlocking {\n        val defaultMediaId = getDefaultFavoriteFolderId(ApiType.Web)\n        val result = favoriteRepository.getFavoriteFolderData(\n            mediaId = defaultMediaId,\n            pageSize = 20,\n            pageNumber = 1,\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get favorite folder data with token`() = runBlocking {\n        val defaultMediaId = getDefaultFavoriteFolderId(ApiType.App)\n        val result = favoriteRepository.getFavoriteFolderData(\n            mediaId = defaultMediaId,\n            pageSize = 20,\n            pageNumber = 1,\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n\n    private suspend fun getDefaultFavoriteFolderId(preferApiType: ApiType): Long {\n        val foldersInfoResult = favoriteRepository.getAllFavoriteFolderMetadataList(\n            mid = UID,\n            preferApiType = preferApiType\n        )\n        val id = foldersInfoResult.find { it.title == \"默认收藏夹\" }?.id ?: 0\n        println(\"default media id: $id\")\n        return id\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/HistoryRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\nimport java.io.File\nimport java.nio.file.Paths\nimport java.util.Properties\n\nclass HistoryRepositoryTest {\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n    }\n\n    private val authRepository = AuthRepository()\n    private val channelRepository = ChannelRepository()\n    private val historyRepository = HistoryRepository(authRepository, channelRepository)\n\n    init {\n        channelRepository.initDefaultChannel(\n            FavoriteRepositoryTest.ACCESS_TOKEN,\n            FavoriteRepositoryTest.BUVID\n        )\n\n        authRepository.sessionData = FavoriteRepositoryTest.SESSDATA\n        authRepository.accessToken = FavoriteRepositoryTest.ACCESS_TOKEN\n        authRepository.biliJct = FavoriteRepositoryTest.BILI_JCT\n    }\n\n    @Test\n    fun `get histories with web api`() = runBlocking {\n        val result = historyRepository.getHistories(\n            cursor = 0,\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get histories with app api`() = runBlocking {\n        val result = historyRepository.getHistories(\n            cursor = 1688955898,\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/PgcRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.pgc.PgcType\nimport dev.aaa1115910.biliapi.entity.pgc.index.Area\nimport dev.aaa1115910.biliapi.entity.pgc.index.Copyright\nimport dev.aaa1115910.biliapi.entity.pgc.index.IndexOrder\nimport dev.aaa1115910.biliapi.entity.pgc.index.IndexOrderType\nimport dev.aaa1115910.biliapi.entity.pgc.index.IsFinish\nimport dev.aaa1115910.biliapi.entity.pgc.index.PgcIndexData\nimport dev.aaa1115910.biliapi.entity.pgc.index.Producer\nimport dev.aaa1115910.biliapi.entity.pgc.index.ReleaseDate\nimport dev.aaa1115910.biliapi.entity.pgc.index.SeasonMonth\nimport dev.aaa1115910.biliapi.entity.pgc.index.SeasonStatus\nimport dev.aaa1115910.biliapi.entity.pgc.index.SeasonVersion\nimport dev.aaa1115910.biliapi.entity.pgc.index.SpokenLanguage\nimport dev.aaa1115910.biliapi.entity.pgc.index.Style\nimport dev.aaa1115910.biliapi.entity.pgc.index.Year\nimport kotlinx.coroutines.runBlocking\nimport kotlin.test.Test\n\nclass PgcRepositoryTest {\n    private val pgcRepository: PgcRepository = PgcRepository()\n\n    @Test\n    fun `get pgc carousel data`() {\n        runBlocking {\n            PgcType.entries.forEach { pgcType ->\n                println(\"pgcType: $pgcType\")\n                val data = pgcRepository.getCarousel(pgcType)\n                println(data)\n            }\n        }\n    }\n\n    @Test\n    fun `get pgc feed data`() {\n        runBlocking {\n            PgcType.entries.forEach { pgcType ->\n                println(\"pgcType: $pgcType\")\n                val data = pgcRepository.getFeed(\n                    pgcType = pgcType,\n                    cursor = 0\n                )\n                println(data)\n            }\n        }\n    }\n\n    @Test\n    fun `get pgc index`() {\n        runBlocking {\n            PgcType.entries.forEach { pgcType ->\n                println(\"pgcType: $pgcType\")\n                val data = pgcRepository.getPgcIndex(\n                    pgcType = pgcType,\n                    indexOrder = IndexOrder.PlayCount,\n                    indexOrderType = IndexOrderType.Desc,\n                    seasonVersion = SeasonVersion.All,\n                    spokenLanguage = SpokenLanguage.All,\n                    area = Area.All,\n                    isFinish = IsFinish.All,\n                    copyright = Copyright.All,\n                    seasonStatus = SeasonStatus.All,\n                    seasonMonth = SeasonMonth.All,\n                    producer = Producer.All,\n                    year = Year.All,\n                    releaseDate = ReleaseDate.All,\n                    style = Style.All,\n                    page = PgcIndexData.PgcIndexPage()\n                )\n                println(data)\n            }\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/RecommendVideoRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.home.RecommendPage\nimport dev.aaa1115910.biliapi.entity.rank.PopularVideoPage\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\nimport java.io.File\nimport java.nio.file.Paths\nimport java.util.Properties\n\nclass RecommendVideoRepositoryTest {\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n    }\n\n    private val authRepository = AuthRepository()\n    private val channelRepository = ChannelRepository()\n    private val recommendVideoRepository =\n        RecommendVideoRepository(authRepository, channelRepository)\n\n    init {\n        channelRepository.initDefaultChannel(\n            FavoriteRepositoryTest.ACCESS_TOKEN,\n            FavoriteRepositoryTest.BUVID\n        )\n\n        authRepository.sessionData = SeasonRepositoryTest.SESSDATA\n        authRepository.accessToken = SeasonRepositoryTest.ACCESS_TOKEN\n        authRepository.biliJct = SeasonRepositoryTest.BILI_JCT\n        authRepository.mid = SeasonRepositoryTest.UID\n    }\n\n    @Test\n    fun getPopularVideos() = runBlocking {\n        val result = recommendVideoRepository.getPopularVideos(\n            page = PopularVideoPage(),\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get popular videos with web api`() = runBlocking {\n        `get popular videos`(5, ApiType.Web)\n    }\n\n    @Test\n    fun `get popular videos with app api`() = runBlocking {\n        `get popular videos`(5, ApiType.App)\n    }\n\n    private suspend fun `get popular videos`(\n        pageCount: Int,\n        preferApiType: ApiType\n    ) = runBlocking {\n        var nextPage = PopularVideoPage()\n        for (i in 0..pageCount) {\n            val result = recommendVideoRepository.getPopularVideos(\n                page = nextPage,\n                preferApiType = preferApiType\n            )\n            nextPage = result.nextPage\n            println(result.list.map { it.title })\n        }\n    }\n\n    @Test\n    fun `get recommend videos with web api`() = runBlocking {\n        `get recommend videos`(5, ApiType.Web)\n    }\n\n    @Test\n    fun `get recommend videos with app api`() = runBlocking {\n        `get recommend videos`(5, ApiType.App)\n    }\n\n    private suspend fun `get recommend videos`(\n        pageCount: Int,\n        preferApiType: ApiType\n    ) = runBlocking {\n        var nextPage = RecommendPage()\n        for (i in 0..pageCount) {\n            println(\"page: $nextPage\")\n            val result = recommendVideoRepository.getRecommendVideos(\n                page = nextPage,\n                preferApiType = preferApiType\n            )\n            nextPage = result.nextPage\n            println(result.items.map { it.title })\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/SearchRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\nimport java.io.File\nimport java.nio.file.Paths\nimport java.util.Properties\nimport java.util.UUID\n\nclass SearchRepositoryTest {\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n    }\n\n    private val authRepository = AuthRepository()\n    private val channelRepository = ChannelRepository()\n    private val searchRepository = SearchRepository(authRepository, channelRepository)\n\n    init {\n        channelRepository.initDefaultChannel(\n            FavoriteRepositoryTest.ACCESS_TOKEN,\n            FavoriteRepositoryTest.BUVID\n        )\n\n        authRepository.sessionData = FavoriteRepositoryTest.SESSDATA\n        authRepository.accessToken = FavoriteRepositoryTest.ACCESS_TOKEN\n        authRepository.biliJct = FavoriteRepositoryTest.BILI_JCT\n        authRepository.buvid3 = \"${UUID.randomUUID()}${(0..9).random()}infoc\"\n    }\n\n    @Test\n    fun `get search hot words with web api`() = runBlocking {\n        val result = searchRepository.getSearchHotwords(\n            limit = 50,\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get search hot words with app api`() = runBlocking {\n        val result = searchRepository.getSearchHotwords(\n            limit = 50,\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get search suggest with web api`() = runBlocking {\n        val result = searchRepository.getSearchSuggest(\n            keyword = \"00\",\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get search suggest with app api`() = runBlocking {\n        val result = searchRepository.getSearchSuggest(\n            keyword = \"00\",\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n\n    @Test\n    fun `search type test`() = runBlocking {\n        val reply = searchRepository.searchType(\n            keyword = \"fate\",\n            type = SearchType.Video,\n            page = SearchTypePage(),\n            tid = 0,\n            order = SearchFilterOrderType.MostComment,\n            duration = SearchFilterDuration.All,\n            preferApiType = ApiType.App\n        )\n        println(reply)\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/SeasonRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.season.FollowingSeasonType\nimport dev.aaa1115910.biliapi.entity.season.TimelineFilter\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\nimport java.io.File\nimport java.nio.file.Paths\nimport java.util.Properties\n\nclass SeasonRepositoryTest {\n\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n    }\n\n    private val authRepository = AuthRepository()\n    private val channelRepository = ChannelRepository()\n    private val seasonRepository = SeasonRepository(authRepository)\n\n    init {\n        channelRepository.initDefaultChannel(\n            FavoriteRepositoryTest.ACCESS_TOKEN,\n            FavoriteRepositoryTest.BUVID\n        )\n\n        authRepository.sessionData = SESSDATA\n        authRepository.accessToken = ACCESS_TOKEN\n        authRepository.biliJct = BILI_JCT\n        authRepository.mid = UID\n    }\n\n    @Test\n    fun `get following seasons with web api`() = runBlocking {\n        val bangumiResult = seasonRepository.getFollowingSeasons(\n            type = FollowingSeasonType.Bangumi,\n            preferApiType = ApiType.Web\n        )\n        val cinemaResult = seasonRepository.getFollowingSeasons(\n            type = FollowingSeasonType.Cinema,\n            preferApiType = ApiType.Web\n        )\n        println(\"bangumiResult: $bangumiResult\")\n        println(\"cinemaResult: $cinemaResult\")\n    }\n\n    @Test\n    fun `get following seasons with app api`() = runBlocking {\n        val bangumiResult = seasonRepository.getFollowingSeasons(\n            type = FollowingSeasonType.Bangumi,\n            preferApiType = ApiType.App\n        )\n        val cinemaResult = seasonRepository.getFollowingSeasons(\n            type = FollowingSeasonType.Cinema,\n            preferApiType = ApiType.App\n        )\n        println(\"bangumiResult: $bangumiResult\")\n        println(\"cinemaResult: $cinemaResult\")\n    }\n\n    @Test\n    fun `get timeline with web api`() = runBlocking {\n        TimelineFilter.webFilters.forEach { filter ->\n            val result = seasonRepository.getTimeline(\n                filter = filter,\n                preferApiType = ApiType.Web\n            )\n            println(\"filter: $filter, result: $result\")\n        }\n    }\n\n    @Test\n    fun `get timeline with app api`() = runBlocking {\n        TimelineFilter.appFilters.forEach { filter ->\n            val result = seasonRepository.getTimeline(\n                filter = filter,\n                preferApiType = ApiType.App\n            )\n            println(\"filter: $filter, result: $result\")\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/UgcRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ugc.UgcType\nimport kotlinx.coroutines.runBlocking\nimport java.io.File\nimport java.nio.file.Paths\nimport java.util.Properties\nimport kotlin.test.Test\n\nclass UgcRepositoryTest {\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n    }\n\n    private val authRepository = AuthRepository()\n    private val ugcRepository: UgcRepository = UgcRepository(authRepository)\n\n    init {\n        authRepository.sessionData = SESSDATA\n        authRepository.accessToken = ACCESS_TOKEN\n        authRepository.biliJct = BILI_JCT\n    }\n\n    @Test\n    fun `get region data`() = runBlocking {\n        UgcType.entries\n            .filter { it.locId != -1 }\n            .forEach { ugcType ->\n                println(\"ugcType: $ugcType\")\n                val result = ugcRepository.getRegionData(ugcType)\n                println(result)\n            }\n    }\n\n    @Test\n    fun `get region more data`() = runBlocking {\n        UgcType.entries\n            .filter { it.locId != -1 }\n            .forEach { ugcType ->\n                println(\"ugcType: $ugcType\")\n                val result = ugcRepository.getRegionMoreData(ugcType)\n                println(result)\n            }\n    }\n}\n"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/UserRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.user.SpaceVideoOrder\nimport dev.aaa1115910.biliapi.entity.user.SpaceVideoPage\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\nimport java.io.File\nimport java.nio.file.Paths\nimport java.util.Properties\n\nclass UserRepositoryTest {\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n    }\n\n    private val authRepository = AuthRepository()\n    private val channelRepository = ChannelRepository()\n    private val userRepository = UserRepository(authRepository, channelRepository)\n\n    init {\n        channelRepository.initDefaultChannel(\n            FavoriteRepositoryTest.ACCESS_TOKEN,\n            FavoriteRepositoryTest.BUVID\n        )\n\n        authRepository.sessionData = FavoriteRepositoryTest.SESSDATA\n        authRepository.accessToken = FavoriteRepositoryTest.ACCESS_TOKEN\n        authRepository.biliJct = FavoriteRepositoryTest.BILI_JCT\n    }\n\n    @Test\n    fun `get user space videos with web api`() = runBlocking {\n        var page = SpaceVideoPage()\n        while (page.hasNext) {\n            val spaceVideoData = userRepository.getSpaceVideos(\n                mid = 2,\n                order = SpaceVideoOrder.PubDate,\n                page = page,\n                preferApiType = ApiType.Web\n            )\n            page = spaceVideoData.page\n            println(\"page $page: $spaceVideoData\")\n            delay((1000L..3000L).random())\n        }\n    }\n\n    @Test\n    fun `get user space videos with app api`() = runBlocking {\n        var page = SpaceVideoPage()\n        while (page.hasNext) {\n            val spaceVideoData = userRepository.getSpaceVideos(\n                mid = 2,\n                order = SpaceVideoOrder.PubDate,\n                page = page,\n                preferApiType = ApiType.App\n            )\n            page = spaceVideoData.page\n            println(\"page $page: $spaceVideoData\")\n            delay((1000L..3000L).random())\n        }\n    }\n\n    @Test\n    fun `get dynamic videos with web api`() = runBlocking {\n        val result = userRepository.getDynamicVideos(\n            page = 1,\n            offset = \"\",\n            updateBaseline = \"\",\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get dynamic videos with grpc api`() = runBlocking {\n        val result = userRepository.getDynamicVideos(\n            page = 1,\n            offset = \"\",\n            updateBaseline = \"\",\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get dynamics with web api`() = runBlocking {\n        val totalPage = 10\n        var historyOffset = \"\"\n        var updateBaseline = \"\"\n        for (i in 1..totalPage) {\n            val dynamicData = userRepository.getDynamics(\n                page = i,\n                offset = historyOffset,\n                updateBaseline = updateBaseline,\n                preferApiType = ApiType.Web\n            )\n            historyOffset = dynamicData.historyOffset\n            updateBaseline = dynamicData.updateBaseline\n            println(\"page $i: ${dynamicData.dynamics.joinToString { it.commentType.toString() }}\")\n            delay((1000L..3000L).random())\n        }\n    }\n\n    @Test\n    fun `get dynamics with grpc api`() = runBlocking {\n        val totalPage = 10\n        var historyOffset = \"\"\n        var updateBaseline = \"\"\n        for (i in 1..totalPage) {\n            val dynamicData = userRepository.getDynamics(\n                page = i,\n                offset = historyOffset,\n                updateBaseline = updateBaseline,\n                preferApiType = ApiType.App\n            )\n            historyOffset = dynamicData.historyOffset\n            updateBaseline = dynamicData.updateBaseline\n            println(\"page $i: ${dynamicData.dynamics.joinToString { it.commentType.toString() }}\")\n            delay((1000L..3000L).random())\n        }\n    }\n\n    @Test\n    fun `get following users with web api`() = runBlocking {\n        val result = userRepository.getFollowedUsers(\n            mid = UID,\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get following users with app api`() = runBlocking {\n        val result = userRepository.getFollowedUsers(\n            mid = UID,\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get dynamic detail`() = runBlocking {\n        ApiType.entries.forEach { apiType ->\n            val result = userRepository.getDynamicDetail(\n                dynamicId = \"946265944846499863\",\n                preferApiType = apiType\n            )\n            println(\"apiType: $apiType\")\n            println(result)\n        }\n    }\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/VideoDetailRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.reply.CommentPage\nimport dev.aaa1115910.biliapi.entity.reply.CommentReplyPage\nimport dev.aaa1115910.biliapi.entity.reply.CommentSort\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\nimport java.io.File\nimport java.nio.file.Paths\nimport java.util.Base64\nimport java.util.Properties\n\nclass VideoDetailRepositoryTest {\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n    }\n\n    private val channelRepository = ChannelRepository()\n    private val authRepository = AuthRepository()\n    private val favoriteRepository = FavoriteRepository(authRepository)\n    private val videoDetailRepository =\n        VideoDetailRepository(authRepository, channelRepository, favoriteRepository)\n\n    init {\n        channelRepository.initDefaultChannel(\n            VideoPlayRepositoryTest.ACCESS_TOKEN,\n            VideoPlayRepositoryTest.BUVID\n        )\n        authRepository.sessionData = VideoPlayRepositoryTest.SESSDATA\n    }\n\n    @Test\n    fun `get video info with http`() {\n        runBlocking {\n            runCatching {\n                val result = videoDetailRepository.getVideoDetail(\n                    aid = 91849600L,\n                    preferApiType = ApiType.Web\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get video info with grpc`() {\n        runBlocking {\n            runCatching {\n                val result = videoDetailRepository.getVideoDetail(\n                    aid = 91849600L,\n                    preferApiType = ApiType.App\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get multi part video info with http`() = runBlocking {\n        val result = videoDetailRepository.getVideoDetail(\n            aid = 836207,\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get multi part video info with grpc`() = runBlocking {\n        val result = videoDetailRepository.getVideoDetail(\n            aid = 836207,\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get ugc season video info with http`() {\n        runBlocking {\n            runCatching {\n                val result = videoDetailRepository.getVideoDetail(\n                    aid = 954251211,\n                    preferApiType = ApiType.Web\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get ugc season video info with grpc`() {\n        runBlocking {\n            runCatching {\n                val result = videoDetailRepository.getVideoDetail(\n                    aid = 954251211,\n                    preferApiType = ApiType.App\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get anime video info with http`() {\n        runBlocking {\n            runCatching {\n                val result = videoDetailRepository.getVideoDetail(\n                    aid = 314583081,\n                    preferApiType = ApiType.Web\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get anime video info with grpc`() {\n        runBlocking {\n            runCatching {\n                val result = videoDetailRepository.getVideoDetail(\n                    aid = 314583081,\n                    preferApiType = ApiType.App\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get argue video info with http`() {\n        runBlocking {\n            runCatching {\n                val result = videoDetailRepository.getVideoDetail(\n                    aid = 996965888,\n                    preferApiType = ApiType.Web\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get argue video info with grpc`() {\n        runBlocking {\n            runCatching {\n                val result = videoDetailRepository.getVideoDetail(\n                    aid = 996965888,\n                    preferApiType = ApiType.App\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get pgc season video prefer web api`() {\n        runBlocking {\n            runCatching {\n                val result = videoDetailRepository.getPgcVideoDetail(\n                    epid = 752900,\n                    preferApiType = ApiType.Web\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get pgc season video prefer app api`() {\n        runBlocking {\n            runCatching {\n                val result = videoDetailRepository.getPgcVideoDetail(\n                    epid = 752900,\n                    preferApiType = ApiType.App\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get video comments`() {\n        runBlocking {\n            runCatching {\n                //val aid = 234771846\n                val aid = 491985062L\n                val sort = CommentSort.Time\n                /*val page = CommentPage(\n                    nextWebPage = \"\"\"{\"type\":1,\"direction\":1,\"Data\":{\"cursor\":90}}\"\"\",\n                    nextAppPage = 90\n                )*/\n                val page = CommentPage()\n                val webResult = videoDetailRepository.getComments(\n                    aid = aid,\n                    sort = sort,\n                    page = page,\n                    preferApiType = ApiType.Web\n                )\n                val appResult = videoDetailRepository.getComments(\n                    aid = aid,\n                    sort = sort,\n                    page = page,\n                    preferApiType = ApiType.App\n                )\n                println(webResult)\n                println(appResult)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get all comments with web api`() = `get all comments`(ApiType.Web)\n\n    @Test\n    fun `get all comments with app api`() = `get all comments`(ApiType.App)\n\n    private fun `get all comments`(apiType: ApiType) {\n        runBlocking {\n            runCatching {\n                var page = CommentPage()\n                //val aid = 234771846\n                val aid = 832519956L\n                val sort = CommentSort.Hot\n                var hasNext = true\n                var index = 1\n\n                while (hasNext) {\n                    val data = videoDetailRepository.getComments(\n                        aid = aid,\n                        sort = sort,\n                        page = page,\n                        preferApiType = apiType\n                    )\n                    page = data.nextPage\n                    data.comments.forEach {\n                        println(\"${index++}\\t${it.content.joinToString(separator = \"\")}\")\n                    }\n                    hasNext = data.hasNext\n                    delay(3000)\n                }\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get video comment reply`() {\n        runBlocking {\n            runCatching {\n                val aid = 499133313L\n                val root = 3323576644\n                val page = CommentReplyPage()\n                val webResult = videoDetailRepository.getCommentReplies(\n                    aid = aid,\n                    commentId = root,\n                    page = page,\n                    preferApiType = ApiType.Web\n                )\n                val appResult = videoDetailRepository.getCommentReplies(\n                    aid = aid,\n                    commentId = root,\n                    page = page,\n                    preferApiType = ApiType.App\n                )\n                println(webResult)\n                println(appResult)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get all replies with web api`() = `get all replies`(ApiType.Web)\n\n    @Test\n    fun `get all replies with app api`() = `get all replies`(ApiType.App)\n\n    private fun `get all replies`(apiType: ApiType) {\n        runBlocking {\n            runCatching {\n                var page = CommentReplyPage()\n                val aid = 234771846L\n                val rpid = 190458721840\n                //val aid = 619910840\n                //val rpid = 191513725712\n                var hasNext = true\n                var index = 1\n\n                while (hasNext) {\n                    val data = videoDetailRepository.getCommentReplies(\n                        aid = aid,\n                        commentId = rpid,\n                        page = page,\n                        sort = CommentSort.Time,\n                        preferApiType = apiType\n                    )\n                    page = data.nextPage\n                    data.replies.forEach {\n                        println(\"${index++}\\t${it.content.joinToString(separator = \"\")}\")\n                    }\n                    hasNext = data.hasNext\n                    delay(3000)\n                }\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun foo() {\n        val base64 =\n            \"AQAAA4IfiwgAAAAAAAAANVRNiiVEDHYQRFx5hrcUp0mlKqnEXVJJEJdzARnsFhqmp4e2FURcibj3BC7nIJ7BG7jwFuYJwuO9R/0kX76f+vjP33/768WnLy5f/XT57t3D/e3li8v3b++f725v3r15/ePd08sf7m/vHl/e3j2/vn9zAzdw+fzy7dPjw9f/n35+uHm6++bx4eHu7e1/+z9/9scnKVzrVI2SMlGwTYMo04xGsuUq0/BkrJMbZAgHnKSsuV1YlzBCiNsI1cFCPlAkMSNBDwdhycGYM0hlY1qO9L05xqg9COfJYYVjYIgpgasU27AlJUv8sOxhsvTMjLEmSC1es5eIkTayKeoVNpfBGVyZeNLnRFBLrpFqVJsqNiOe7SlzUp5MjYWOx1d64D59EudABCgzdoqTSzQXLpk1hguacAIcG9NRCAEnMU8pWj5qEQ52RVieizbobDSIsmJJ8zkUa1/5sp2h4yhRcWNYOYOPEPnqiVdoZXOS68zmNHmdmAB2JWCjKoj6HsGNW/PQ3rgXXUdxPjAW5DwqHus0VJj9LwFHbJugPrrpbI0BRFarIAJNUw3vzV4j5MZT3nI1V6zUK3PHiv7VQyjjhB3cMBlltrS2xlgl6d53Ynmx1NRsao5nLj99mCAqDHcUYiF7FZxpsJupPGptOApeCksjGvYagFdf9LWpdViVaR9DmNZfqHvvLgwqzLhiIs9VM52GHE4+bD7gFMFUXnhEhOooxtg9hUBpgPeHvIxCT5fdaDSdRNsSu+fpusbG2si3WFtyiI6ME73haUCxdel0Z9/eorYE4bD5hPb5bKdWe9od+mqjBFmmNkhO0OjoMBB0iEi3r3YchLd/DmBOtWYvRyRGsykalhHdADZFj5ktvE3maL2Cx9VuSGSMDaD9uvp4UesNp4NL2fk0VPeNPtp8OkI6nO3ESluZbQFMxaySuXWygmOAdLhbAmiV+j7LwDWy1eqhTtdTbpSddvAmb9go81PQL4Tu9m62UwzaDXa1+6EOfL8HTHiNklQL0JPIbhuEi16jzZuXJfncNU5xswK7dk+OBoMtRjcy593d+wHawV1ksUYnQHyuBu9Yjaa8rSO2O/zsPXYTs2fSOs2e43IfGqmTqlM+20NHF0cP0nmJJqi17qelq3e72O3OxoBgtAlJrMNK5R/9/ev7f355/+WHrz74F6hB/06DBQAA\"\n\n        //base64 to bytearray\n        val bytes = convertBase64StringToByteArray(base64)\n\n        val req = bilibili.main.community.reply.v1.MainListReq.parseFrom(bytes)\n        println()\n        println(req)\n    }\n\n    @Test\n    fun foo2() {\n        val base64 = \"Cg42N+S6uuato+WcqOecixAeGAEqEjY35Lq65ZCM5pe25Zyo55yLfjoCNjc=\"\n        val bytes = convertBase64StringToByteArray(base64)\n\n        val req = bilibili.app.playeronline.v1.PlayerOnlineReply.parseFrom(bytes)\n        println()\n        println(req)\n    }\n\n    fun convertBase64StringToByteArray(base64String: String): ByteArray {\n        return Base64.getDecoder().decode(base64String)\n    }\n\n\n}"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/repositories/VideoPlayRepositoryTest.kt",
    "content": "package dev.aaa1115910.biliapi.repositories\n\nimport bilibili.rpc.Status\n//import com.google.rpc.Status\nimport dev.aaa1115910.biliapi.entity.ApiType\nimport dev.aaa1115910.biliapi.entity.video.HeartbeatVideoType\nimport dev.aaa1115910.biliapi.grpc.utils.getDetail\nimport dev.aaa1115910.biliapi.http.BiliHttpProxyApi\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\nimport java.io.File\nimport java.net.URL\nimport java.nio.file.Paths\nimport java.util.Properties\nimport kotlin.io.encoding.Base64\nimport kotlin.io.encoding.ExperimentalEncodingApi\n\nclass VideoPlayRepositoryTest {\n    companion object {\n        private val localProperties = Properties().apply {\n            val path = Paths.get(\"../local.properties\").toAbsolutePath().toString()\n            load(File(path).bufferedReader())\n        }\n        val SESSDATA: String =\n            runCatching { localProperties.getProperty(\"test.sessdata\") }.getOrNull() ?: \"\"\n        val BILI_JCT: String =\n            runCatching { localProperties.getProperty(\"test.bili_jct\") }.getOrNull() ?: \"\"\n        val UID: Long =\n            runCatching { localProperties.getProperty(\"test.uid\") }.getOrNull()?.toLongOrNull() ?: 2\n        val ACCESS_TOKEN: String =\n            runCatching { localProperties.getProperty(\"test.access_token\") }.getOrNull() ?: \"\"\n        val BUVID: String =\n            runCatching { localProperties.getProperty(\"test.buvid\") }.getOrNull() ?: \"\"\n        val HTTP_PROXY_SERVER: String =\n            runCatching { localProperties.getProperty(\"test.http_proxy_server\") }.getOrNull() ?: \"\"\n        val GRPC_PROXY_SERVER: String =\n            runCatching { localProperties.getProperty(\"test.grpc_proxy_server\") }.getOrNull() ?: \"\"\n    }\n\n    private val authRepository = AuthRepository()\n    private val channelRepository = ChannelRepository()\n    private val videoPlayRepository = VideoPlayRepository(authRepository, channelRepository)\n\n    init {\n        channelRepository.initDefaultChannel(ACCESS_TOKEN, BUVID)\n        channelRepository.initProxyChannel(ACCESS_TOKEN, BUVID, GRPC_PROXY_SERVER)\n        BiliHttpProxyApi.createClient(HTTP_PROXY_SERVER)\n        authRepository.sessionData = SESSDATA\n        authRepository.accessToken = ACCESS_TOKEN\n        authRepository.biliJct = BILI_JCT\n    }\n\n    @Test\n    fun `get flac video with grpc`() {\n        runBlocking {\n            runCatching {\n                val result = videoPlayRepository.getPlayData(\n                    aid = 993403941,\n                    cid = 1051761130,\n                    preferApiType = ApiType.App\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get flac video with http`() {\n        runBlocking {\n            runCatching {\n                val result = videoPlayRepository.getPlayData(\n                    aid = 993403941,\n                    cid = 1051761130,\n                    preferApiType = ApiType.Web\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get 8k video with grpc`() {\n        runBlocking {\n            runCatching {\n                val result = videoPlayRepository.getPlayData(\n                    aid = 934637444,\n                    cid = 455439756,\n                    preferApiType = ApiType.App\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get 8k video with http`() {\n        runBlocking {\n            runCatching {\n                val result = videoPlayRepository.getPlayData(\n                    aid = 934637444,\n                    cid = 455439756,\n                    preferApiType = ApiType.Web\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get multi part video with http`() = runBlocking {\n        val result = videoPlayRepository.getPlayData(\n            aid = 836207,\n            cid = 1215693,\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get multi part video with grpc`() = runBlocking {\n        val result = videoPlayRepository.getPlayData(\n            aid = 836207,\n            cid = 1215693,\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n\n    @OptIn(ExperimentalEncodingApi::class)\n    @Test\n    fun `parse error status`() {\n        val errorBin =\n            \"CAISBC00MDQaRAondHlwZS5nb29nbGVhcGlzLmNvbS9iaWxpYmlsaS5ycGMuU3RhdHVzEhkI7Pz/////////ARIM5ZWl6YO95pyo5pyJ\"\n        val errorData = Base64.decode(errorBin)\n        val status = Status.parseFrom(errorData).getDetail()\n        println(status)\n    }\n\n    @Test\n    fun `get pgc video with grpc`() {\n        runBlocking {\n            runCatching {\n                val result = videoPlayRepository.getPgcPlayData(\n                    aid = 210680503,\n                    cid = 486114279,\n                    epid = 469110,\n                    preferApiType = ApiType.App\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get pgc video with http`() {\n        runBlocking {\n            runCatching {\n                val result = videoPlayRepository.getPgcPlayData(\n                    aid = 210680503,\n                    cid = 486114279,\n                    epid = 469110,\n                    preferApiType = ApiType.Web\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get paid pgc video with grpc`() {\n        runBlocking {\n            runCatching {\n                val result = videoPlayRepository.getPgcPlayData(\n                    aid = 741219885,\n                    cid = 1132332811,\n                    epid = 750015,\n                    preferApiType = ApiType.App\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get paid pgc video with http`() {\n        runBlocking {\n            runCatching {\n                val result = videoPlayRepository.getPgcPlayData(\n                    aid = 741219885,\n                    cid = 1132332811,\n                    epid = 750015,\n                    preferApiType = ApiType.Web\n                )\n                println(result)\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    @Test\n    fun `get subtitle with web api`() = runBlocking {\n        val result = videoPlayRepository.getSubtitle(\n            aid = 913498989,\n            cid = 1203020250,\n            preferApiType = ApiType.Web\n        )\n        println(result)\n    }\n\n    @Test\n    fun `get subtitle with app api`() = runBlocking {\n        val result = videoPlayRepository.getSubtitle(\n            aid = 913498989,\n            cid = 1203020250,\n            preferApiType = ApiType.App\n        )\n        println(result)\n    }\n\n    @Test\n    fun `send heartbeat with web api`() = runBlocking {\n        val randomTime = (0..100).random()\n        println(\"random time: $randomTime\")\n        videoPlayRepository.sendHeartbeat(\n            aid = 170001,\n            cid = 280468,\n            time = randomTime,\n            preferApiType = ApiType.Web\n        )\n        videoPlayRepository.sendHeartbeat(\n            aid = 476982015,\n            cid = 1107179650,\n            type = HeartbeatVideoType.Season,\n            subType = 4,\n            time = randomTime,\n            epid = 706666,\n            seasonId = 39707,\n            preferApiType = ApiType.Web\n        )\n    }\n\n    @Test\n    fun `send heartbeat with app api`() = runBlocking {\n        val randomTime = (0..100).random()\n        println(\"random time: $randomTime\")\n        videoPlayRepository.sendHeartbeat(\n            aid = 170001,\n            cid = 280468,\n            time = randomTime,\n            preferApiType = ApiType.App\n        )\n        videoPlayRepository.sendHeartbeat(\n            aid = 476982015,\n            cid = 1107179650,\n            type = HeartbeatVideoType.Season,\n            subType = 4,\n            time = randomTime,\n            epid = 706666,\n            seasonId = 39707,\n            preferApiType = ApiType.App\n        )\n    }\n\n    @Test\n    fun `get region limited pgc play data`() = runBlocking {\n        val cid = 1199794768L\n        val epid = 763396\n        val enableProxy = true\n        val proxyArea = \"hk\"\n        val webResult = videoPlayRepository.getPgcPlayData(\n            aid = 0,\n            cid = cid,\n            epid = epid,\n            preferApiType = ApiType.Web,\n            enableProxy = enableProxy,\n            proxyArea = proxyArea\n        )\n        val appResult = videoPlayRepository.getPgcPlayData(\n            aid = 0,\n            cid = cid,\n            epid = epid,\n            preferApiType = ApiType.App,\n            enableProxy = enableProxy,\n            proxyArea = proxyArea\n        )\n        println(\"web result: $webResult\")\n        println(\"app result: $appResult\")\n    }\n\n    @Test\n    fun `get play url domain`() = runBlocking {\n        val getUrlDomain: (String) -> String = {\n            val url = URL(it)\n            \"${url.protocol}://${url.host}\"\n        }\n        ApiType.entries.forEach { apiType ->\n            val result = videoPlayRepository.getPlayData(\n                aid = 934637444,\n                cid = 455439756,\n                preferApiType = apiType\n            )\n            println(\"api type: $apiType\")\n\n            result.dashVideos.forEach { video ->\n                println(\"video quality: ${video.quality}\")\n                val videoUrls = mutableListOf<String>()\n                videoUrls.add(video.baseUrl)\n                videoUrls.addAll(video.backUrl)\n                videoUrls.forEach { println(getUrlDomain(it)) }\n            }\n        }\n    }\n\n    @Test\n    fun `get video shots`() = runBlocking {\n        ApiType.entries.forEach { apiType ->\n            val result = videoPlayRepository.getVideoShot(\n                aid = 170001,\n                cid = 279786,\n                preferApiType = apiType\n            )\n            println(\"api type: $apiType\")\n            println(result)\n        }\n    }\n}\n\n"
  },
  {
    "path": "bili-api/src/test/kotlin/dev/aaa1115910/biliapi/websocket/LiveDataWebSocketTest.kt",
    "content": "package dev.aaa1115910.biliapi.websocket\n\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\n\ninternal class LiveDataWebSocketTest {\n\n    @Test\n    fun connectLiveEvent() {\n        runBlocking {\n            LiveDataWebSocket.connectLiveEvent(5555) {\n                println(it)\n            }\n            for (i in 1..10) {\n                delay(1_000)\n            }\n        }\n    }\n}"
  },
  {
    "path": "bili-subtitle/.gitignore",
    "content": "/build"
  },
  {
    "path": "bili-subtitle/build.gradle.kts",
    "content": "plugins {\n    alias(gradleLibs.plugins.kotlin.jvm)\n    alias(gradleLibs.plugins.kotlin.serialization)\n}\n\ndependencies {\n    implementation(libs.kotlinx.serialization)\n    testImplementation(libs.kotlin.test)\n}\n\ntasks.test {\n    useJUnitPlatform()\n}\n"
  },
  {
    "path": "bili-subtitle/src/main/kotlin/dev/aaa1115910/bilisubtitle/SubtitleEncoder.kt",
    "content": "package dev.aaa1115910.bilisubtitle\n\nimport dev.aaa1115910.bilisubtitle.entity.BiliSubtitle\nimport dev.aaa1115910.bilisubtitle.entity.BiliSubtitleItem\nimport dev.aaa1115910.bilisubtitle.entity.SrtSubtitleItem\nimport dev.aaa1115910.bilisubtitle.entity.SubtitleItem\nimport kotlinx.serialization.encodeToString\nimport kotlinx.serialization.json.Json\n\nobject SubtitleEncoder {\n    fun encodeToBcc(subtitles: List<SubtitleItem>): String {\n        val bccSubtitles = mutableListOf<BiliSubtitleItem>()\n        subtitles.forEach {\n            bccSubtitles.add(\n                BiliSubtitleItem(\n                    from = it.from.getBccTime(),\n                    to = it.to.getBccTime(),\n                    location = 2,\n                    content = it.content\n                )\n            )\n        }\n        val bccSubtitle = BiliSubtitle(\n            fontSize = 0.4f,\n            fontColor = \"#FFFFFF\",\n            backgroundAlpha = 0.5f,\n            backgroundColor = \"#9C27B0\",\n            stroke = \"none\",\n            body = bccSubtitles\n        )\n        return Json.encodeToString(bccSubtitle)\n    }\n\n    fun encodeToSrt(subtitles: List<SubtitleItem>): String {\n        var result = \"\"\n        subtitles.forEachIndexed { index, data ->\n            val srtSubtitleItem = SrtSubtitleItem(\n                index = index + 1,\n                from = data.from.getSrtTime(),\n                to = data.to.getSrtTime(),\n                content = data.content.replace(\"\\n\",\"\\\\n\")\n            )\n            if (index != 0) result += \"\\n\"\n            result += srtSubtitleItem.toRaw()\n        }\n        return result\n    }\n}"
  },
  {
    "path": "bili-subtitle/src/main/kotlin/dev/aaa1115910/bilisubtitle/SubtitleParser.kt",
    "content": "package dev.aaa1115910.bilisubtitle\n\nimport dev.aaa1115910.bilisubtitle.entity.BiliSubtitle\nimport dev.aaa1115910.bilisubtitle.entity.SubtitleItem\nimport dev.aaa1115910.bilisubtitle.entity.Timestamp\nimport kotlinx.serialization.decodeFromString\nimport kotlinx.serialization.json.Json\n\nobject SubtitleParser {\n    private val bccJson = Json {\n        ignoreUnknownKeys = true\n    }\n\n    fun fromBccString(bcc: String, isAI: Boolean = false): List<SubtitleItem> {\n        val result = mutableListOf<SubtitleItem>()\n        val bccResult = runCatching {\n            bccJson.decodeFromString<BiliSubtitle>(bcc)\n        }.onFailure { e ->\n            println(\"[SubtitleParser] BCC decode failed: ${e.message}\")\n        }.getOrNull()\n        bccResult?.body?.forEach { bccItem ->\n            result.add(\n                SubtitleItem(\n                    from = Timestamp.fromBccString(bccItem.from),\n                    to = Timestamp.fromBccString(bccItem.to),\n                    content = bccItem.content,\n                    isAI = isAI\n                )\n            )\n        }\n        return result\n    }\n\n    fun fromSrtString(srt: String, isAI: Boolean = false): List<SubtitleItem> {\n        val result = mutableListOf<SubtitleItem>()\n        val lines = srt.lines()\n        var position = 0\n        while (position < lines.size) {\n            @Suppress(\"UNUSED_VARIABLE\")\n            val index = lines[position++]\n            val timeLines = lines[position++]\n            val content = lines[position++]\n            position++\n            val times = timeLines.split(\" \")\n            val from = times.first()\n            val to = times.last()\n            result.add(\n                SubtitleItem(\n                    from = Timestamp.fromSrtString(from),\n                    to = Timestamp.fromSrtString(to),\n                    content = content,\n                    isAI = isAI\n                )\n            )\n        }\n        return result\n    }\n}\n"
  },
  {
    "path": "bili-subtitle/src/main/kotlin/dev/aaa1115910/bilisubtitle/entity/BiliSubtitle.kt",
    "content": "package dev.aaa1115910.bilisubtitle.entity\n\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonNames\n\n@OptIn(ExperimentalSerializationApi::class)\n@Serializable\ndata class BiliSubtitle constructor(\n    @SerialName(\"font_size\")\n    val fontSize: Float? = null,\n    @SerialName(\"font_color\")\n    val fontColor: String? = null,\n    @SerialName(\"background_alpha\")\n    val backgroundAlpha: Float? = null,\n    @SerialName(\"background_color\")\n    val backgroundColor: String? = null,\n    // AI字幕返回的属性是大写的（Stroke），非AI字幕是小写的（stroke）\n    // 兼容大小写写法，序列化输出使用小写 stroke\n    @SerialName(\"Stroke\")\n    @JsonNames(\"stroke\")\n    val stroke: String? = null,\n    val type: String? = null,\n    val lang: String? = null,\n    val version: String? = null,\n    val body: List<BiliSubtitleItem> = emptyList()\n)\n\n@Serializable\ndata class BiliSubtitleItem(\n    val from: Float,\n    val to: Float,\n    val sid: Int? = null,\n    val location: Int? = null,\n    val content: String,\n    val music: Float? = null,\n    val version: String? = null // 自动翻译字幕特有属性\n)"
  },
  {
    "path": "bili-subtitle/src/main/kotlin/dev/aaa1115910/bilisubtitle/entity/SrtSubtitle.kt",
    "content": "package dev.aaa1115910.bilisubtitle.entity\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SrtSubtitle(\n    val content: List<SrtSubtitleItem> = emptyList()\n)\n\n@Serializable\ndata class SrtSubtitleItem(\n    val index: Int,\n    val from: String,\n    val to: String,\n    val content: String\n) {\n    fun toRaw() = \"\"\"\n        $index\n        $from --> $to\n        $content\n        \n    \"\"\".trimIndent()\n}\n"
  },
  {
    "path": "bili-subtitle/src/main/kotlin/dev/aaa1115910/bilisubtitle/entity/SubtitleItem.kt",
    "content": "package dev.aaa1115910.bilisubtitle.entity\n\ndata class SubtitleItem(\n    val from: Timestamp,\n    val to: Timestamp,\n    val content: String,\n    val isAI: Boolean = false  // 是否为 AI 生成字幕\n) {\n    fun isShowing(time: Long) = from.totalMills <= time && to.totalMills >= time\n}\n"
  },
  {
    "path": "bili-subtitle/src/main/kotlin/dev/aaa1115910/bilisubtitle/entity/Timestamp.kt",
    "content": "package dev.aaa1115910.bilisubtitle.entity\n\ndata class Timestamp(\n    val hours: Int,\n    val minutes: Int,\n    val seconds: Int,\n    val milliSeconds: Int,\n    var totalMills: Long = 0\n) {\n    companion object {\n        fun fromSrtString(srtTime: String): Timestamp {\n            val parts = srtTime.split(\":\", \",\")\n            return Timestamp(\n                hours = parts[0].toInt(),\n                minutes = parts[1].toInt(),\n                seconds = parts[2].toInt(),\n                milliSeconds = parts[3].toInt()\n            )\n        }\n\n        fun fromBccString(bccTime: Float): Timestamp {\n            val mils = (bccTime * 1000).toInt()\n            val hours = mils / (1000 * 60 * 60)\n            val minutes = (mils % (1000 * 60 * 60)) / (1000 * 60)\n            val seconds = (mils % (1000 * 60)) / (1000)\n            val milliSeconds = mils % 1000\n            return Timestamp(hours, minutes, seconds, milliSeconds)\n        }\n    }\n\n    init {\n        totalMills = hours * 60 * 60 * 1000L + minutes * 60 * 1000L + seconds * 1000L + milliSeconds\n    }\n\n    fun getBccTime(): Float =\n        hours * 60 * 60 + minutes * 60 + seconds + milliSeconds / 1000f\n\n    fun getSrtTime(): String {\n        val h = String.format(\"%02d\", hours)\n        val m = String.format(\"%02d\", minutes)\n        val s = String.format(\"%02d\", seconds)\n        var ms = \"$milliSeconds\"\n        while (ms.length < 3) {\n            ms += \"0\"\n        }\n        return \"$h:$m:$s,$ms\"\n    }\n}"
  },
  {
    "path": "bili-subtitle/src/test/kotlin/dev/aaa1115910/bilisubtitle/SubtitleEncoderTest.kt",
    "content": "package dev.aaa1115910.bilisubtitle\n\nimport java.io.File\nimport kotlin.test.Test\n\nclass SubtitleEncoderTest {\n\n    @Test\n    fun `encode to bcc`() {\n        val fileContent = this::class.java.getResource(\"/example.srt\")?.readText()!!\n        val data = SubtitleParser.fromSrtString(fileContent)\n        val result = SubtitleEncoder.encodeToBcc(data)\n        val outputFile = File(\"build/output.bcc\")\n        outputFile.createNewFile()\n        outputFile.writeText(result)\n    }\n\n    @Test\n    fun `encode to srt`() {\n        val fileContent = this::class.java.getResource(\"/example.bcc\")?.readText()!!\n        val data = SubtitleParser.fromBccString(fileContent)\n        val result = SubtitleEncoder.encodeToSrt(data)\n        val outputFile = File(\"build/output.srt\")\n        outputFile.createNewFile()\n        outputFile.writeText(result)\n    }\n}"
  },
  {
    "path": "bili-subtitle/src/test/kotlin/dev/aaa1115910/bilisubtitle/SubtitleParserTest.kt",
    "content": "package dev.aaa1115910.bilisubtitle\n\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertTrue\n\nclass SubtitleParserTest {\n    @Test\n    fun `read bcc subtitle`() {\n        val fileContent = this::class.java.getResource(\"/example.bcc\")?.readText()!!\n        val result = SubtitleParser.fromBccString(fileContent)\n        println(result)\n    }\n\n    @Test\n    fun `read srt subtitle`() {\n        val fileContent = this::class.java.getResource(\"/example.srt\")?.readText()!!\n        val result = SubtitleParser.fromSrtString(fileContent)\n        println(result)\n    }\n\n        @Test\n        fun `read ai bcc subtitle without location`() {\n                val fileContent = \"\"\"\n                        {\n                            \"font_size\": 0.4,\n                            \"font_color\": \"#FFFFFF\",\n                            \"background_alpha\": 0.5,\n                            \"background_color\": \"#9C27B0\",\n                            \"Stroke\": \"none\",\n                            \"type\": \"AIsubtitle\",\n                            \"lang\": \"pt\",\n                            \"version\": \"1.0\",\n                            \"body\": [\n                                {\n                                    \"from\": 0.5,\n                                    \"to\": 2.0,\n                                    \"content\": \"Ola mundo\",\n                                    \"sid\": 1,\n                                    \"version\": \"1.0\"\n                                }\n                            ]\n                        }\n                \"\"\".trimIndent()\n\n                val result = SubtitleParser.fromBccString(fileContent, isAI = true)\n\n                assertEquals(1, result.size)\n                assertEquals(\"Ola mundo\", result.first().content)\n                assertTrue(result.first().isAI)\n        }\n}"
  },
  {
    "path": "bili-subtitle/src/test/kotlin/dev/aaa1115910/bilisubtitle/entity/TimestampTest.kt",
    "content": "package dev.aaa1115910.bilisubtitle.entity\n\nimport kotlin.test.Test\n\nclass TimestampTest {\n    @Test\n    fun `parse bcc time`() {\n        val time = 7530.2f\n        println(Timestamp.fromBccString(time))\n    }\n\n    @Test\n    fun `parse srt time`() {\n        val time = \"01:03:17,775\"\n        println(Timestamp.fromSrtString(time))\n    }\n}"
  },
  {
    "path": "bili-subtitle/src/test/resources/example.bcc",
    "content": "{\"font_size\":0.4,\"font_color\":\"#FFFFFF\",\"background_alpha\":0.5,\"background_color\":\"#9C27B0\",\"Stroke\":\"none\",\"body\":[{\"from\":1.01,\"to\":6.02,\"location\":2,\"content\":\"大家好 我是普林斯顿大学计算机科学\\n教授 Bob Sedgewick(鲍勃·赛德维克)\"},{\"from\":6.02,\"to\":11.01,\"location\":2,\"content\":\"这门算法课是由我和在普林斯顿大学\\n的Kevin Wayne(凯文·韦恩)开设的在线课程\"},{\"from\":11.01,\"to\":16,\"location\":2,\"content\":\"首先我们会简要概述你们需要学习算法的理由\"},{\"from\":16,\"to\":21,\"location\":2,\"content\":\"然后会提一下本门课程所需要的材料\"},{\"from\":21,\"to\":26.54,\"location\":2,\"content\":\"那么 这门课到底教什么？这门课是算法的中级概论课\\n（Survey Course，大学里对没学过相关主题的学生提供的一种入门课）\"},{\"from\":26.54,\"to\":31.06,\"location\":2,\"content\":\"我们会着重讲算法的程序实现以及实际问题中算法的应用\"},{\"from\":31.06,\"to\":35.87,\"location\":2,\"content\":\"所以我们的两个重点是\"},{\"from\":35.87,\"to\":41.76,\"location\":2,\"content\":\"用来解决问题的方法即算法 以及\"},{\"from\":41.76,\"to\":46.65,\"location\":2,\"content\":\"与算法关系密切的用来保存问题中信息的数据结构\"},{\"from\":46.65,\"to\":51.6,\"location\":2,\"content\":\"以下是我们要在这门课第一部分和第二部分所要讲述的内容\"},{\"from\":51.6,\"to\":56.56,\"location\":2,\"content\":\"第一部分要讲基本数据结构、排序和查找\"},{\"from\":56.56,\"to\":60.81,\"location\":2,\"content\":\"包括一些最基础的数据结构和算法\"},{\"from\":60.81,\"to\":65.81,\"location\":2,\"content\":\"数据结构部分包括栈、队列、背包、优先队列\"},{\"from\":65.81,\"to\":69.85,\"location\":2,\"content\":\"算法部分涵盖了排序的经典算法\"},{\"from\":69.85,\"to\":75.13,\"location\":2,\"content\":\"包括快速排序、归并排序、堆排序、基数排序\"},{\"from\":75.13,\"to\":79.93,\"location\":2,\"content\":\"还有经典的查找算法包括二叉查找树、红黑树、哈希表（散列表）\"},{\"from\":79.93,\"to\":85.17,\"location\":2,\"content\":\"第二部分我们会讲更高级的算法\"},{\"from\":85.17,\"to\":90.42,\"location\":2,\"content\":\"包括图论算法 经典的图的搜索算法、最小生成树和最短路径\"},{\"from\":90.42,\"to\":95.42,\"location\":2,\"content\":\"字符串处理的算法\"},{\"from\":95.42,\"to\":101.18,\"location\":2,\"content\":\"包括正则表达式和数据压缩\"},{\"from\":101.18,\"to\":106.89,\"location\":2,\"content\":\"然后会讲一些使用前面课程讲解过的基本算法的高级算法\"},{\"from\":106.89,\"to\":112.08,\"location\":2,\"content\":\"那么我们为什么要学算法？因为它们产生的影响非常深远\"},{\"from\":112.08,\"to\":117.19,\"location\":2,\"content\":\"从互联网到生物学、商业计算、计算机图形\"},{\"from\":117.19,\"to\":122.51,\"location\":2,\"content\":\"安全、多媒体、社交网络、科学应用等领域，算法都无处不在\"},{\"from\":122.51,\"to\":127.81,\"location\":2,\"content\":\"它们可以用来制作电影、游戏 模拟粒子碰撞\"},{\"from\":127.81,\"to\":132.91,\"location\":2,\"content\":\"研究基因组等等许多其他的应用\"},{\"from\":132.91,\"to\":138.32,\"location\":2,\"content\":\"可见算法所影响的领域非常广泛\\n这是我们要学习算法的一个重要原因\"},{\"from\":138.32,\"to\":143.33,\"location\":2,\"content\":\"算法也非常有趣，它有深厚的历史渊源\"},{\"from\":143.33,\"to\":148.06,\"location\":2,\"content\":\"欧几里德研究了第一个算法 至少可以追溯到公元前300年\\n（最大公约数算法）\"},{\"from\":148.06,\"to\":152.59,\"location\":2,\"content\":\"在上世纪三十年代，算法的概念就在普里斯顿\"},{\"from\":152.59,\"to\":157.65,\"location\":2,\"content\":\"由丘奇和图灵正式确定下来\"},{\"from\":157.65,\"to\":161.81,\"location\":2,\"content\":\"但大部分我们要学习的算法都是近几十年发明的\"},{\"from\":161.81,\"to\":166.45,\"location\":2,\"content\":\"实际上有一些算法就是像我们这门课的\\n课堂上由本科生发明的\"},{\"from\":166.45,\"to\":171.59,\"location\":2,\"content\":\"还有许多算法留待被你们发现\"},{\"from\":171.59,\"to\":177.24,\"location\":2,\"content\":\"研究算法主要是为了解决那些非算法不可解决的问题\"},{\"from\":177.24,\"to\":182.5,\"location\":2,\"content\":\"比如 第一堂课我们要讲\"},{\"from\":182.5,\"to\":187.23,\"location\":2,\"content\":\"网络连通性问题。在一个大的对象集合中\"},{\"from\":187.23,\"to\":192.13,\"location\":2,\"content\":\"对象成对相连 我们想知道能否经过这些连接从一个对象\"},{\"from\":192.13,\"to\":197.13,\"location\":2,\"content\":\"到达另一个对象 从这个例子你可以看出\"},{\"from\":197.13,\"to\":202.28,\"location\":2,\"content\":\"有没有这样一条路径不是明显的\\n我们需要一个计算机程序来求解\"},{\"from\":202.28,\"to\":208.79,\"location\":2,\"content\":\"而且我们需要的是一个高效的算法\\n这个情形中结果存在这样的路径\"},{\"from\":208.79,\"to\":214.24,\"location\":2,\"content\":\"学习算法的另一个原因是可以启发智力\"},{\"from\":214.24,\"to\":219.79,\"location\":2,\"content\":\"研究算法非常有趣\"},{\"from\":219.79,\"to\":225.28,\"location\":2,\"content\":\"高德纳（Don Knuth）是算法领域的先驱\\n他写过很多算法相关的书籍\"},{\"from\":225.28,\"to\":230.55,\"location\":2,\"content\":\"他曾经说 “算法亲眼见到可行了才能信赖”\"},{\"from\":230.55,\"to\":236,\"location\":2,\"content\":\"你不能光思考算法 还要动手去做\"},{\"from\":236,\"to\":241.41,\"location\":2,\"content\":\"Francis Sullivan说过 “好算法是计算的诗篇”\"},{\"from\":241.41,\"to\":245.91,\"location\":2,\"content\":\"像散文一样 算法可以是简洁的、飘渺的、凝练的甚至是神秘的\"},{\"from\":245.91,\"to\":251.47,\"location\":2,\"content\":\"但一旦神秘被揭开，算法就启发计算的某个方面 算法吸引人的地方就在于它启迪智慧\"},{\"from\":251.47,\"to\":257.06,\"location\":2,\"content\":\"我猜你们许多人学算法的另一个理由是\"},{\"from\":257.06,\"to\":261.56,\"location\":2,\"content\":\"要成为一个技术娴熟的程序员\"},{\"from\":261.56,\"to\":266.76,\"location\":2,\"content\":\"懂得优秀的算法、高效的算法、合适的数据结构是必须的\"},{\"from\":266.76,\"to\":271.36,\"location\":2,\"content\":\"发明Linux的Linus Torvalds说过\"},{\"from\":271.36,\"to\":276.71,\"location\":2,\"content\":\"程序员的差距就体现在他觉得代码更重要还是数据结构更重要\"},{\"from\":276.71,\"to\":281.74,\"location\":2,\"content\":\"拙劣的程序员关心代码 而优秀的程序员\"},{\"from\":281.74,\"to\":285.94,\"location\":2,\"content\":\"关心数据结构 以及它们之间的关系 我想在这里补充一下\"},{\"from\":285.94,\"to\":290.05,\"location\":2,\"content\":\"还有处理这些关系的算法\\nNiklaus Wirth 另一位计算机科学的先驱\"},{\"from\":290.05,\"to\":295.44,\"location\":2,\"content\":\"著名的Algorithms+Data Structures = Programs一书的作者\"},{\"from\":295.44,\"to\":302.08,\"location\":2,\"content\":\"现在研究算法的另一个原因是，算法已经是\"},{\"from\":302.08,\"to\":308.42,\"location\":2,\"content\":\"用来了解自然的一种通用语言。算法是计算性的模型\"},{\"from\":308.42,\"to\":314.89,\"location\":2,\"content\":\"在科学研究中，算法模型正在取代数学模型\"},{\"from\":314.89,\"to\":320.29,\"location\":2,\"content\":\"二十世纪科学家建立数学模型来尝试理解自然现象\"},{\"from\":320.29,\"to\":325.25,\"location\":2,\"content\":\"很快大家发现数学模型很难求解\"},{\"from\":325.25,\"to\":330.56,\"location\":2,\"content\":\"求解并针对自然现象验证假设是很困难的\"},{\"from\":330.56,\"to\":336.01,\"location\":2,\"content\":\"所以现在越来越多的人\"},{\"from\":336.01,\"to\":341.19,\"location\":2,\"content\":\"在发展计算性的模型 尝试去模拟\"},{\"from\":341.19,\"to\":346.57,\"location\":2,\"content\":\"自然中可能发生的事情来试着更好地理解它\"},{\"from\":346.57,\"to\":352.07,\"location\":2,\"content\":\"算法在这个过程中扮演了非常重要的角色 在这门课中\"},{\"from\":352.07,\"to\":358.01,\"location\":2,\"content\":\"会有这样的一些例子 另一个重要的原因是如果你知道\"},{\"from\":358.01,\"to\":363.07,\"location\":2,\"content\":\"如何高效的使用算法和数据结构 你将有\"},{\"from\":363.07,\"to\":369.85,\"location\":2,\"content\":\"更好地机会通过面试获取一个科技工业界的职位\"},{\"from\":369.85,\"to\":375.95,\"location\":2,\"content\":\"所以 我刚刚说了一堆学习算法的原因\"},{\"from\":375.95,\"to\":381.76,\"location\":2,\"content\":\"算法影响深远，有古老的根源又提供崭新的机会，\"},{\"from\":381.76,\"to\":386.18,\"location\":2,\"content\":\"使我们能解决无法以其他方式解决的问题\"},{\"from\":386.18,\"to\":390,\"location\":2,\"content\":\"你可以使用算法提高自己的智力\"},{\"from\":390,\"to\":394.05,\"location\":2,\"content\":\"成为一名专业的程序员 算法可能揭开宇宙中生命的奥秘\"},{\"from\":394.05,\"to\":398.46,\"location\":2,\"content\":\"算法能带来乐趣和利益 开个玩笑 一个专业的程序员或许会问\"},{\"from\":398.46,\"to\":402.43,\"location\":2,\"content\":\"为什么要学别的东西？当然 学习其他东西也有大量充分原因的\"},{\"from\":402.43,\"to\":407.56,\"location\":2,\"content\":\"但我没有不学习算法的充分理由 关于这门课\"},{\"from\":407.56,\"to\":413.71,\"location\":2,\"content\":\"我要讲下这两个资源 确保你们\"},{\"from\":413.71,\"to\":419.37,\"location\":2,\"content\":\"在进入正题之前做好准备 这是我和Kevin Wayne发展并已经\"},{\"from\":419.37,\"to\":424.47,\"location\":2,\"content\":\"应用了很多年的出版模式 我们认为\"},{\"from\":424.47,\"to\":429.51,\"location\":2,\"content\":\"这是一种支持我们将要讲授的课程的很有效的方式\"},{\"from\":429.51,\"to\":434.66,\"location\":2,\"content\":\"下面这个是对应的可选的课本\"},{\"from\":434.66,\"to\":439.89,\"location\":2,\"content\":\"这是一部传统的教科书，详尽地覆盖了这门课的主题\"},{\"from\":439.89,\"to\":444.29,\"location\":2,\"content\":\"实际上包括了比我们在这门课里能讲的多得多的主题\"},{\"from\":444.29,\"to\":448.67,\"location\":2,\"content\":\"同时还有那个课本配套的免费在线资料\"},{\"from\":448.67,\"to\":453.84,\"location\":2,\"content\":\"在这里你能找到这门课的讲义 但是更重要的是，\"},{\"from\":453.84,\"to\":459.97,\"location\":2,\"content\":\"这里有代码和练习 以及非常多的信息\"},{\"from\":459.97,\"to\":465.33,\"location\":2,\"content\":\"可能是书中内容的十倍 包括内容总结。所以\"},{\"from\":465.33,\"to\":471.21,\"location\":2,\"content\":\"在这门课程中，你在线学习的时候将会经常参考这个网站\"},{\"from\":471.21,\"to\":477.07,\"location\":2,\"content\":\"人们经常会问到这门课需要的先修知识。我们假设\"},{\"from\":477.07,\"to\":482.82,\"location\":2,\"content\":\"学习这门课程的人知道如何编程 知道基本的循环、数组和函数\"},{\"from\":482.82,\"to\":489.31,\"location\":2,\"content\":\"并且接触过面向对象编程和递归\"},{\"from\":489.31,\"to\":495.71,\"location\":2,\"content\":\"我们用Java语言 但是我们不会细致地讲Java 主要使用Java\"},{\"from\":495.71,\"to\":501.55,\"location\":2,\"content\":\"作为描述性的语言。需要一些数学基础 但不包括高等数学。如果你想\"},{\"from\":501.55,\"to\":507.88,\"location\":2,\"content\":\"复习一些我们认为是这门课程的先修知识的资料，\"},{\"from\":507.88,\"to\":513.4,\"location\":2,\"content\":\"你可以在书的章节1.1和1.2中做一个快速的回顾\"},{\"from\":513.4,\"to\":519.11,\"location\":2,\"content\":\"不管是在线的还是纸质的。如果你需要一个深度的复习，\"},{\"from\":519.11,\"to\":523.72,\"location\":2,\"content\":\"我们有一本书叫做《An Introduction to Programming in Java: An\"},{\"from\":523.72,\"to\":529.34,\"location\":2,\"content\":\"Interdisciplinary Approach》，同样有电子版和纸质版。但是，\"},{\"from\":529.34,\"to\":533.97,\"location\":2,\"content\":\"底线是 你至少要会编程 一个快速的准备方法是\"},{\"from\":533.97,\"to\":538.69,\"location\":2,\"content\":\"在你的计算机上使用书中提到的编程模型\"},{\"from\":538.69,\"to\":543.29,\"location\":2,\"content\":\"写一个Java程序。我们将会在我们布置作业的时候\"},{\"from\":543.29,\"to\":547.85,\"location\":2,\"content\":\"提供更多的详细信息。你可以使用你自己习惯的\"},{\"from\":547.85,\"to\":553.29,\"location\":2,\"content\":\"编程环境或者使用我们提供的编程环境。在网页上有\"},{\"from\":553.29,\"to\":555.04,\"location\":2,\"content\":\"一些指导\"}]}"
  },
  {
    "path": "bili-subtitle/src/test/resources/example.srt",
    "content": "1\n00:00:00,000 --> 00:00:06,050\n本字幕由小熊字幕组施工\n\n2\n00:00:06,050 --> 00:00:11,050\n高速战斗的刚体破绽诀别\n\n3\n00:02:39,375 --> 00:02:43,075\n准备MMD（MMM不能用）\n\n4\n00:02:43,075 --> 00:02:45,475\n推荐使用PMXEditor\n\n5\n00:02:45,475 --> 00:02:47,525\n使用模型为にがもん式霊夢\n\n6\n00:02:47,525 --> 00:02:52,025\n本教程主力工具，针金P的MikuMIkuMob\n\n7\n00:02:52,025 --> 00:02:56,100\n这次教程的UP主是Yomi大大\n\n8\n00:02:56,100 --> 00:03:01,100\n这次解说配音是小灵梦\n\n9\n00:03:01,100 --> 00:03:04,850\n使用PE载入模型\n\n10\n00:03:04,850 --> 00:03:09,250\n如果模型没有全ての親，追加\n\n11\n00:03:09,250 --> 00:03:12,775\nctrl+I 追加骨骼，命名为“操作中心”，打开“移动”\n\n12\n00:03:12,775 --> 00:03:17,775\n另存为“Mob用”模型，准备下一步\n\n13\n00:03:17,775 --> 00:03:22,775\n打开MikuMikuMob，拖入Mob用”模型\n\n14\n00:03:22,775 --> 00:03:27,775\n范围设定 L1、L2、H2设为0.01\n\n15\n00:03:28,275 --> 00:03:33,275\n配置设定，勾选随机，对象数=1\n\n16\n00:03:33,275 --> 00:03:38,275\n导出设定，把“追加刚体”前的勾，去掉\n\n17\n00:03:38,275 --> 00:03:43,275\n导出模型，命名为“高速刚体用”，进行下一步\n\n18\n00:03:43,275 --> 00:03:48,275\nMikuMikuMob操作结束，打开PE\n\n19\n00:03:52,150 --> 00:03:57,150\n模型名变更\n\n20\n00:04:01,425 --> 00:04:04,300\n接着追加刚体和joint\n\n21\n00:04:04,300 --> 00:04:09,300\n再打开一个PE，载入改造前的模型\n\n22\n00:04:09,300 --> 00:04:14,300\n全选改造前模型的刚体，复制到剪贴板\n\n23\n00:04:14,300 --> 00:04:17,575\n粘贴到改造中模型\n\n24\n00:04:17,575 --> 00:04:22,575\n接着复制joint\n\n25\n00:04:22,575 --> 00:04:27,575\n接着是重点环节，瞪大眼睛看喽\n\n26\n00:04:27,575 --> 00:04:30,325\n骨骼改造\n\n27\n00:04:30,325 --> 00:04:35,325\n删除——0：全ての親\n\n28\n00:04:44,575 --> 00:04:50,050\n删除——201：0001全ての親和202：0001操作中心\n\n29\n00:04:51,675 --> 00:04:57,975\n选中——201：0001全ての親 （这里不解释，203-202=201），变形阶层设为0，关掉“付与”的旋转+和移动+\n\n30\n00:04:58,350 --> 00:05:07,775\n将201：0001全ての親 ，重命名为“全ての親”，将亲骨骼设为“-1” （此步关键，原视频中有提示，作者忘记改了）\n\n31\n00:05:08,275 --> 00:05:17,650\n选中——1：全ての親，重命名为“0：全ての親（物理）”\n\n32\n00:05:24,800 --> 00:05:52,650\n追加エッジ骨（edge骨），为了防止全ての親多段化引起的边缘变粗的问题（参考：im2830775）\n\n33\n00:05:53,075 --> 00:05:58,825\n选中——1：全ての\n\n34\n00:05:58,825 --> 00:06:04,525\n选中——20\n\n35\n00:06:04,525 --> 00:06:07,275\n返回，选中1：edge調整用，亲骨骼设为205——edge用dummy，将变形阶层设为4\n\n36\n00:06:07,275 --> 00:06:15,725\n将edge調整用的坐标设为与4：上半身坐标相同\n\n37\n00:06:15,725 --> 00:06:18,925\n到此骨骼操作结束，设定表示枠\n\n38\n00:06:18,925 --> 00:06:23,925\n打开“表示枠”，如图追加\n\n39\n00:06:24,050 --> 00:06:29,450\n全親（見）——20\n\n40\n00:06:29,450 --> 00:06:34,450\n打开MMD，载入模型\n\n41\n00:09:19,375 --> 00:09:24,375\n文字篇教程见\\nhtt\n\n42\n00:09:24,375 --> 00:09:29,375\n插件篇教程见\\nhtt\n\n43\n00:09:29,375 --> 00:09:34,375\n目前的插件不够方便，日后我会制作便利的插件\n\n44\n00:09:34,375 --> 00:09:39,375\n借物表\n\n45\n00:09:39,375 --> 00:09:44,375\nMME的借物 作者和特效名 都可以写\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "plugins {\n    alias(gradleLibs.plugins.android.application) apply false\n    alias(gradleLibs.plugins.android.library) apply false\n    alias(gradleLibs.plugins.compose.compiler) apply false\n    alias(gradleLibs.plugins.google.ksp) apply false\n    alias(gradleLibs.plugins.kotlin.android) apply false\n    alias(gradleLibs.plugins.kotlin.jvm) apply false\n    alias(gradleLibs.plugins.kotlin.serialization) apply false\n    alias(gradleLibs.plugins.versions)\n}"
  },
  {
    "path": "doc/弹幕/calc_danmaku_averages.js",
    "content": "#!/usr/bin/env node\n\n// 运行命令：node doc/calc_danmaku_averages.js doc/优化前的.txt\n// 指定只统计前 N 条匹配日志：node doc/calc_danmaku_averages.js doc/优化前的.txt 100\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst inputPath = process.argv[2];\nconst maxLogsArg = process.argv[3];\n\nif (!inputPath) {\n  console.error('Usage: node doc/calc_danmaku_averages.js <log-file> [max-logs]');\n  process.exit(1);\n}\n\nconst maxLogs = parseMaxLogs(maxLogsArg);\n\nconst resolvedPath = path.resolve(process.cwd(), inputPath);\n\nlet content;\ntry {\n  content = fs.readFileSync(resolvedPath, 'utf8');\n} catch (error) {\n  console.error(`Failed to read file: ${resolvedPath}`);\n  console.error(error.message);\n  process.exit(1);\n}\n\nconst pattern = /\\[(Draw|Action)\\]\\s+fps=(\\d+(?:\\.\\d+)?)\\s+frames=(\\d+)\\s+dropped=(\\d+)/;\nconst memoryPattern = /mem=heap=(\\d+(?:\\.\\d+)?)\\/(\\d+(?:\\.\\d+)?)MB\\s+native=(\\d+(?:\\.\\d+)?)MB/;\nconst cacheQPattern = /cacheQ=(\\d+)/;\nconst actMsPattern = /actMs\\(avg\\/p50\\/p95\\/max\\)=(\\d+(?:\\.\\d+)?)\\/(\\d+(?:\\.\\d+)?)\\/(\\d+(?:\\.\\d+)?)\\/(\\d+(?:\\.\\d+)?)/;\nconst snapPattern = /sameSnap=(\\d+)\\s+maxSameSnapStreak=(\\d+)\\s+snapAgeMs\\(avg\\/max\\)=(\\d+(?:\\.\\d+)?)\\/(\\d+(?:\\.\\d+)?)/;\nconst stats = {\n  Draw: createBucket(),\n  Action: createBucket(),\n};\nconst memoryStats = createMemoryBucket();\nconst cacheQStats = { count: 0, total: 0, min: Infinity, max: -Infinity };\nlet matchedLogs = 0;\n\nfor (const line of content.split(/\\r?\\n/)) {\n  const match = line.match(pattern);\n  if (!match) {\n    continue;\n  }\n  if (maxLogs !== null && matchedLogs >= maxLogs) {\n    break;\n  }\n\n  matchedLogs += 1;\n\n  const memoryMatch = line.match(memoryPattern);\n  if (memoryMatch) {\n    const [, heapUsed, heapMax, nativeUsed] = memoryMatch;\n    memoryStats.count += 1;\n    memoryStats.heapUsed += Number.parseFloat(heapUsed);\n    memoryStats.heapMax += Number.parseFloat(heapMax);\n    memoryStats.nativeUsed += Number.parseFloat(nativeUsed);\n  }\n\n  const cacheQMatch = line.match(cacheQPattern);\n  if (cacheQMatch) {\n    const val = Number.parseInt(cacheQMatch[1], 10);\n    cacheQStats.count += 1;\n    cacheQStats.total += val;\n    if (val < cacheQStats.min) cacheQStats.min = val;\n    if (val > cacheQStats.max) cacheQStats.max = val;\n  }\n\n  const [, type, fps, frames, dropped] = match;\n  const bucket = stats[type];\n  bucket.count += 1;\n  bucket.fps += Number.parseFloat(fps);\n  bucket.frames += Number.parseInt(frames, 10);\n  bucket.dropped += Number.parseInt(dropped, 10);\n\n  if (type === 'Action') {\n    const actMsMatch = line.match(actMsPattern);\n    if (actMsMatch) {\n      bucket.actMsCount += 1;\n      bucket.actMsAvg += Number.parseFloat(actMsMatch[1]);\n      bucket.actMsP50 += Number.parseFloat(actMsMatch[2]);\n      bucket.actMsP95 += Number.parseFloat(actMsMatch[3]);\n      bucket.actMsMax += Number.parseFloat(actMsMatch[4]);\n    }\n  }\n\n  if (type === 'Draw') {\n    const snapMatch = line.match(snapPattern);\n    if (snapMatch) {\n      const sameSnap = Number.parseInt(snapMatch[1], 10);\n      const maxSameSnapStreak = Number.parseInt(snapMatch[2], 10);\n      bucket.snapCount += 1;\n      bucket.sameSnap += sameSnap;\n      bucket.maxSameSnapStreak += maxSameSnapStreak;\n      if (sameSnap > 0) bucket.sameSnapPositiveCount += 1;\n      if (maxSameSnapStreak > bucket.maxMaxSameSnapStreak) bucket.maxMaxSameSnapStreak = maxSameSnapStreak;\n      bucket.snapAgeAvg += Number.parseFloat(snapMatch[3]);\n      bucket.snapAgeMax += Number.parseFloat(snapMatch[4]);\n    }\n  }\n}\n\nprintStats('Draw', stats.Draw);\nprintStats('Action', stats.Action);\nprintMemoryStats(memoryStats);\nprintCacheQStats(cacheQStats);\n\nfunction parseMaxLogs(value) {\n  if (value == null) {\n    return null;\n  }\n\n  if (!/^\\d+$/.test(value)) {\n    console.error('max-logs must be a positive integer');\n    process.exit(1);\n  }\n\n  const parsed = Number.parseInt(value, 10);\n  if (parsed <= 0) {\n    console.error('max-logs must be greater than 0');\n    process.exit(1);\n  }\n\n  return parsed;\n}\n\nfunction createBucket() {\n  return {\n    count: 0,\n    fps: 0,\n    frames: 0,\n    dropped: 0,\n    actMsCount: 0,\n    actMsAvg: 0,\n    actMsP50: 0,\n    actMsP95: 0,\n    actMsMax: 0,\n    snapCount: 0,\n    sameSnap: 0,\n    maxSameSnapStreak: 0,\n    maxMaxSameSnapStreak: 0,\n    sameSnapPositiveCount: 0,\n    snapAgeAvg: 0,\n    snapAgeMax: 0,\n  };\n}\n\nfunction createMemoryBucket() {\n  return {\n    count: 0,\n    heapUsed: 0,\n    heapMax: 0,\n    nativeUsed: 0,\n  };\n}\n\nfunction average(total, count) {\n  if (count === 0) {\n    return 'N/A';\n  }\n\n  return (total / count).toFixed(2);\n}\n\nfunction printStats(type, bucket) {\n  console.log(`${type}:`);\n\n  if (bucket.count === 0) {\n    console.log('  no matched lines');\n    return;\n  }\n\n  console.log(`  samples: ${bucket.count}`);\n  console.log(`  avg fps: ${average(bucket.fps, bucket.count)}`);\n  console.log(`  avg frames: ${average(bucket.frames, bucket.count)}`);\n  console.log(`  avg dropped: ${average(bucket.dropped, bucket.count)}`);\n\n  if (bucket.actMsCount > 0) {\n    console.log(`  avg actMs(avg): ${average(bucket.actMsAvg, bucket.actMsCount)}ms`);\n    console.log(`  avg actMs(p50): ${average(bucket.actMsP50, bucket.actMsCount)}ms`);\n    console.log(`  avg actMs(p95): ${average(bucket.actMsP95, bucket.actMsCount)}ms`);\n    console.log(`  avg actMs(max): ${average(bucket.actMsMax, bucket.actMsCount)}ms`);\n  }\n\n  if (bucket.snapCount > 0) {\n    console.log(`  avg sameSnap: ${average(bucket.sameSnap, bucket.snapCount)}`);\n    console.log(`  avg maxSameSnapStreak: ${average(bucket.maxSameSnapStreak, bucket.snapCount)}`);\n    console.log(`  max maxSameSnapStreak: ${bucket.maxMaxSameSnapStreak}`);\n    console.log(`  records with sameSnap>0: ${bucket.sameSnapPositiveCount}`);\n    console.log(`  avg snapAgeMs(avg): ${average(bucket.snapAgeAvg, bucket.snapCount)}ms`);\n    console.log(`  avg snapAgeMs(max): ${average(bucket.snapAgeMax, bucket.snapCount)}ms`);\n  }\n}\n\nfunction printCacheQStats(bucket) {\n  console.log('CacheQ:');\n\n  if (bucket.count === 0) {\n    console.log('  no matched lines');\n    return;\n  }\n\n  console.log(`  samples: ${bucket.count}`);\n  console.log(`  avg cacheQ: ${average(bucket.total, bucket.count)}`);\n  console.log(`  min cacheQ: ${bucket.min}`);\n  console.log(`  max cacheQ: ${bucket.max}`);\n}\n\nfunction printMemoryStats(bucket) {\n  console.log('Memory:');\n\n  if (bucket.count === 0) {\n    console.log('  no matched lines');\n    return;\n  }\n\n  console.log(`  samples: ${bucket.count}`);\n  console.log(`  avg heap used: ${average(bucket.heapUsed, bucket.count)}MB`);\n  console.log(`  avg heap max: ${average(bucket.heapMax, bucket.count)}MB`);\n  console.log(`  avg native used: ${average(bucket.nativeUsed, bucket.count)}MB`);\n}"
  },
  {
    "path": "doc/弹幕/弹幕code review 报告.md",
    "content": "\n# 弹幕系统 Code Review 报告\n\n## 总览\n\n整体架构为 **三线程 + 双缓冲快照** 设计（Main Thread 绘制 / ActionThread 逻辑 / CacheThread 缓存构建），是一套高性能、精心设计的弹幕引擎。核心代码质量较高，ECS-like 的分离设计合理。以下内容按 **已实施 / 尝试后放弃 / 未实施** 分类整理，尽量保留原始问题描述，并将重复问题合并到对应条目中。\n\n---\n\n## 一、已实施\n\n### 优化 1：消除时间链路 Int 截断（亚像素抖动修复）\n\n将整个时间数据链路从 `Long`/`Int` 精度提升到 `Double`：\n- DanmakuTimer.kt — `step()` 和 `currentPositionMs()` 返回 `Double`\n- DanmakuEngine.kt — `currentPositionMs`、`stepTime()`、`scrollX()` 全部使用 `Double`\n- RenderSnapshot.kt — `positionMs` 改为 `Double`\n\n**效果**：60fps 下每帧位移从 4.8\\~5.4px 交替抖动变为稳定 5.3px；120Hz 设备改善更明显。\n\n### 2. Mobile 端 `initDanmakuConfig` 未同步 `minLevel`（FilterLevel）\n\n**文件**：[BvPlayer.kt (Mobile)](d:\\code\\bv\\player\\mobile\\src\\main\\kotlin\\dev\\aaa1115910\\bv\\player\\mobile\\BvPlayer.kt#L121-L131)\n\n```kotlin\nval initDanmakuConfig: () -> Unit = {\n    danmakuConfig = danmakuConfig.copy(\n        enabled = true,   // ← 硬编码 true，不读 showDanmaku\n        // 缺少: minLevel = filterLevel\n    )\n}\n```\n\n对比 TV 端的 `syncDanmakuConfig`：\n- TV 端读取 `videoPlayerConfigData.showDanmaku` 作为 `enabled`\n- TV 端设置了 `minLevel = filterLevel`\n- Mobile 端硬编码 `enabled = true`，且未同步 `minLevel`\n\n**结果**：Mobile 端弹幕等级过滤设置无法生效；初始化时 `enabled` 不受 `showDanmaku` 控制。\n\n### 3. Mobile 端缺少 `speedLevel` / `rollingDurationFactor` 映射\n\n**文件**：[BvPlayer.kt (Mobile)](d:\\code\\bv\\player\\mobile\\src\\main\\kotlin\\dev\\aaa1115910\\bv\\player\\mobile\\BvPlayer.kt)\n\nMobile 端的 `DanmakuMenu` 只暴露了 4 项设置（类型、不透明度、区域、大小），但 `DanmakuConfig.speedLevel` 始终保持默认值 `4`。即使 ViewModel 中存在 `currentDanmakuRollingDurationFactor`，Mobile 端也没有同步这个值到 `DanmakuConfig`。\n\n### 6. `opacity` 和 `area` 通过 `configProvider` lambda 动态提供，但未触发 `updateConfig`\n\n**文件**：[BvPlayer.kt (TV)](d:\\code\\bv\\player\\tv\\src\\main\\kotlin\\dev\\aaa1115910\\bv\\player\\tv\\BvPlayer.kt#L559-L564)\n\n```kotlin\nsetConfigProvider {\n    danmakuConfig.copy(\n        opacity = currentConfigData.currentDanmakuOpacity,\n        area = currentConfigData.currentDanmakuArea,\n    )\n}\n```\n\n`DanmakuView.onDraw()` 每帧调用 `configProvider` 获取 config，然后与 `lastConfig` 比较来决定是否 `player.updateConfig(cfg)`。由于 `opacity` 和 `area` 通过 `copy()` 注入，**只要 opacity/area 变化就会触发 config 更新**。但 `updateConfig()` 内部会调 `seekTo(currentPositionMs())`——这意味着**每次修改不透明度的滑块，都会导致所有弹幕重新生成（清屏+重新 spawn）**。这在用户拖动滑块调整透明度时会造成弹幕闪烁。\n\n**建议**：`opacity` 不影响布局，不应该触发 `seekTo`。在 `DanmakuPlayer` 的 `MSG_OP_CONFIG` handler 中，应区分「仅外观变化」和「布局变化」。如果只是 opacity 变了，不需要 seekTo。\n\n### 7. `syncDanmakuConfig` 未同步 `speedLevel`\n\n**文件**：[BvPlayer.kt (TV)](d:\\code\\bv\\player\\tv\\src\\main\\kotlin\\dev\\aaa1115910\\bv\\player\\tv\\BvPlayer.kt#L254-L267)\n\nTV 端的 `syncDanmakuConfig()` 同步了 `enabled`, `textSizeScale`, `allowScroll/Top/Bottom`, `minLevel`，但**没有同步 `speedLevel`**（来自 `currentDanmakuRollingDurationFactor`）。这意味着如果用户切换视频后重新 sync，speedLevel 会回到默认值 4。\n\n### 8. Mobile 端弹幕区域用 `Modifier.fillMaxHeight(area)` 而非 `DanmakuConfig.area`\n\n**文件**：[BvPlayer.kt (Mobile)](d:\\code\\bv\\player\\mobile\\src\\main\\kotlin\\dev\\aaa1115910\\bv\\player\\mobile\\BvPlayer.kt)\n\n```kotlin\nModifier.fillMaxHeight(videoPlayerConfigData.currentDanmakuArea)\n```\n\nMobile 通过裁切 View 高度来限制弹幕区域，这与 TV 端使用 `DanmakuConfig.area` 在引擎内部控制 `usableHeight` 的方式不同。**两者行为不一致**：Mobile 裁切 View 会影响蒙版绘制区域，TV 则不会。建议统一方式。\n\n### 9. **[高优先级]** `drawTextDirect()` 每条弹幕逐条切换 `textSize` 导致高频 Paint 重建\n\n**文件**：DanmakuEngine.kt\n\n```kotlin\nprivate fun drawTextDirect(...) {\n    val effectiveTs = (textSizePx * clampedSize / 25f * scaleFactor).coerceAtLeast(1f)\n    if (drawFill.textSize != effectiveTs) { drawFill.textSize = effectiveTs; drawStroke.textSize = effectiveTs }\n    drawFill.getFontMetrics(drawFontMetrics)  // ← 每条都调 getFontMetrics\n    // ...\n    if (drawFill.textSize != baseTs) { drawFill.textSize = baseTs; drawStroke.textSize = baseTs }  // ← 恢复\n}\n```\n\n`drawTextDirect` 是**缓存未命中时的 fallback**。当大量弹幕首次出现且缓存还没准备好时，每条弹幕都会触发：\n1. Paint.textSize 切换 × 2（设置 + 恢复）\n2. `getFontMetrics()` 调用\n3. `drawText()` × 2（stroke + fill）\n\n在弹幕密集场景（如高能弹幕段）可能一帧有 20-30 条 fallback 绘制。\n\n**建议**：\n- 将 `drawTextDirect` 中的弹幕按 `effectiveTs` 分组批量绘制，减少 Paint 切换次数\n- 或者提高缓存构建优先级 `MAX_CACHE_REQUESTS_PER_FRAME`（当前仅 8），减少 fallback 频率\n\n#### 原后续优化记录：3. 提高缓存构建速率\n\n测试过，MAX_CACHE_REQUESTS_PER_FRAME 8个就够用了，加大一点 改12吧\n\n`MAX_CACHE_REQUESTS_PER_FRAME` 从 8 → 12，减少弹幕密集段的 `drawTextDirect` fallback 频率，降低 Paint 频繁切换导致的 GPU pipeline flush。\n\n### 10. **[高优先级]** 蒙版 `saveLayer()` 每帧触发离屏渲染\n\n**文件**：DanmakuView.kt\n\n```kotlin\nval saveCount = canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null)\nplayer.draw(canvas, rawPos, isPlaying, speed, cfg)\ndrawMaskBitmap(canvas, maskBitmap, videoAspectRatio, areaRatio)\ncanvas.restoreToCount(saveCount)\n```\n\n`saveLayer()` 会触发 GPU 离屏缓冲区分配，这是 Android 绘制管线中**最昂贵的操作之一**。每帧都调用此操作（只要蒙版启用），在低端设备上会导致明显掉帧。\n\n**建议**：\n- 考虑将蒙版应用改为 `canvas.clipPath()` + `Region.Op.INTERSECT`，避免离屏渲染\n- 或使用 `setLayerType(LAYER_TYPE_HARDWARE, ...)` 将整个 View 做硬件层缓存，在蒙版 bitmap 不变时可跳过重建\n\n#### 原后续优化记录：优化 5：蒙版 saveLayer 优化\n\n**改动文件**：DanmakuView.kt、DanmakuMaskModifiers.kt\n\n- **DanmakuView**：蒙版激活时通过 `setLayerType(LAYER_TYPE_HARDWARE)` 让 View 渲染到持久 GPU FBO，DstIn 操作直接在硬件层内完成，不需要每帧 `saveLayer`；蒙版移除时恢复 `LAYER_TYPE_NONE`。保留 saveLayer 作为硬件层尚未生效时的 fallback\n- **DanmakuMaskModifiers (Compose)**：将手动 `canvas.saveLayer()` + `canvas.restore()` 替换为 `Modifier.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }.drawWithContent { ... }`，由 Compose 框架管理离屏缓冲生命周期\n- **效果**：避免每帧分配/释放 GPU 离屏缓冲区，持久 FBO 跨帧复用，减少 GPU pipeline flush\n\n### 11. **[中等]** `appendDanmakus()` 重置 `index=0` 和 `clearActives()` 导致弹幕闪烁\n\n**文件**：DanmakuEngine.kt\n\n```kotlin\nfun appendDanmakus(list: List<Danmaku>, maxItems: Int, alreadySorted: Boolean) {\n    // ...\n    rebuildFilteredItems()\n    index = 0; clearActives(); pending.clear(); lastNowMs = 0  // ← 重置一切\n    publishEmptySnapshot()\n}\n```\n\n追加弹幕时清空所有活跃弹幕并从头开始 → 正在屏幕上滚动的弹幕会瞬间消失。如果段切换场景使用 `appendDanmakus`，实际上每次段切换都会清屏。\n\n**建议**：`appendDanmakus()` 应使用 `lowerBound()` 重新定位 `index` 到当前播放位置附近，而不是重置为 0。活跃弹幕可通过时间范围检查保留仍在屏幕中的项。\n\n**实施结果**：中间插入时改为重新计算 `index`，同时保留 `actives` / `pending`，不再清空 snapshot。\n\n---\n\n## 二、尝试后放弃\n\n### 12. **[中等]** `act()` 中的 snapshot 发布阻塞在 `drawSemaphore.acquire()`\n\n**文件**：DanmakuPlayer.kt\n\n```kotlin\nMSG_FRAME_UPDATE -> {\n    drawSemaphore.acquire()  // ← 如果 main 线程还没 draw 就一直阻塞\n    engine.act()\n    view.postInvalidateOnAnimation()\n}\n```\n\nActionThread 会在 `drawSemaphore.acquire()` 上阻塞，直到上一帧 main 线程的 `draw()` 调用 `releaseSemaphoreIfNeeded()`。如果 main 线程因其他 UI 工作延迟了 `onDraw`，ActionThread 就白白等待，**浪费了一个 vsync 周期**。\n\n**建议**：改用 `tryAcquire(frameTimeout)` 带超时，或使用 AtomicReference 交换 snapshot 引用的无锁方案替代 Semaphore。\n\n#### 原后续优化记录：优化 4：Semaphore → 无锁三缓冲\n\n> 试过了，效果不好\n\n**改动文件**：DanmakuEngine.kt、DanmakuPlayer.kt\n\n- 完全移除 `Semaphore`，ActionThread 不再阻塞等待 Main Thread 完成绘制\n- 双缓冲改为**三缓冲**（`snapshotA/B/C`），通过 `@Volatile readingSnapshot` 追踪 Main Thread 正在读取的 buffer，`writableSnapshot()` 始终选择一个既不是最新发布、也不是正在读取的 buffer\n- Main Thread 调用 `acquireSnapshot()` 标记正在读取，`releaseSnapshot()` 释放，全程无锁\n- **效果**：消除原来 ActionThread 12ms 超时等待的帧丢弃问题；Main Thread 即使偶尔延迟也不会阻塞 ActionThread 的帧计算\n\n#### 原理说明\n\n#### 原方案：Semaphore 同步双缓冲\n\n原来有两个线程交替操作两个 snapshot buffer（A/B）：\n\n```text\nActionThread（生产者）                MainThread（消费者）\n      │                                    │\n      ▼                                    ▼\n drawSemaphore.acquire(12ms)          drawSemaphore.tryAcquire()\n      │  ← 阻塞等待 Main 读完 ──────  读 snapshot\n      ▼                                    │\n engine.act() 写入 snapshot              draw(canvas, snapshot)\n      │                                    │\n      ▼                                    ▼\n [写完了，等下一个 vsync]           releaseSemaphoreIfNeeded()\n                                     └→ release() 给 ActionThread 解阻\n```\n\n**问题**：\n\n1. **ActionThread 被阻塞** — 每帧开始时必须 `acquire(12ms)` 等 Main Thread 上一帧读完，如果 Main Thread 因布局/GC 等原因延迟 `onDraw`，ActionThread 白等 12ms 后超时，**整帧计算被跳过**\n2. **双缓冲的限制** — 只有 A/B 两个 buffer，写之前必须确认读方不在用，否则数据竞争\n\n#### 新方案：无锁三缓冲\n\n三个 buffer（A/B/C）+ 两个 `@Volatile` 指针：\n\n```kotlin\n@Volatile var latestSnapshot: RenderSnapshot   // 最近一次写完的（ActionThread 发布）\n@Volatile var readingSnapshot: RenderSnapshot?  // Main Thread 正在读的（可能为 null）\n```\n\n**写入（ActionThread）**：\n\n```kotlin\nfun act() {\n    val out = writableSnapshot()  // 选一个安全的 buffer\n    // ... 填充 out ...\n    latestSnapshot = out          // volatile 写 → 原子发布\n}\n\nfun writableSnapshot(): RenderSnapshot {\n    // 选一个既不是 latest（刚发布的）也不是 reading（正在被读的）的 buffer\n    return when {\n        A !== latest && A !== reading -> A\n        B !== latest && B !== reading -> B\n        else -> C\n    }\n}\n```\n\n**读取（Main Thread）**：\n\n```kotlin\nfun draw(canvas, ...) {\n    val snapshot = engine.acquireSnapshot()  // reading = latest\n    engine.draw(canvas, snapshot, ...)\n    engine.releaseSnapshot()                // reading = null\n}\n```\n\n- ActionThread **永远不阻塞**，每个 vsync 都执行 `act()` 计算新一帧\n- Main Thread 延迟了也无所谓，ActionThread 继续往第三个 buffer 写\n- Main Thread 恢复后读到的是最新的 snapshot，中间的旧帧自然被跳过（和显示器跳帧一样）\n\n**核心收益**：再也没有\"ActionThread 等 12ms 超时后丢弃整帧\"的情况，弹幕位置更新不会因 Main Thread 偶尔繁忙而中断。\n\n---\n\n## 三、未实施\n\n本节包含允许的不改、误判，以及尚未继续推进的项。\n\n### 4. `DanmakuEngine.act()` 中 `actionPaint.textSize` 被 Spawn 循环中的 `measureTextWidth()` 篡改\n\n> 允许的，不改\n\n**文件**：DanmakuEngine.kt\n\n```kotlin\nprivate fun measureTextWidth(item: DanmakuItem, outlinePad: Float, cfg: DanmakuConfig): Float {\n    val clampedSize = min(item.data.textSize, 25)\n    val effectiveTextSizePx = (textSizePx * clampedSize / 25f * scaleFactor).coerceAtLeast(1f)\n    actionPaint.textSize = effectiveTextSizePx  // ← 修改了共享 Paint\n    return actionPaint.measureText(text) + outlinePad * 2f\n}\n```\n\n`act()` 开头将 `actionPaint.textSize = layoutTextSizePx` 用于计算 `textBoxHeight`、`laneHeight` 等布局参数。但之后 spawn 循环中调用 `measureTextWidth()` 修改了 `actionPaint.textSize` 为每条弹幕自己的 effective size。**如果有弹幕 textSize != 25，布局计算的 laneHeight 就和实际文字高度不一致**。当前 B 站弹幕 textSize 绝大多数为 25，允许存在字体大小不同的弹幕。\n\n### 5. `DanmakuPlayer.draw()` 中的信号量使用可能导致帧丢失\n\n> 这个逻辑才是对的。是AI误判。\n\n**文件**：DanmakuPlayer.kt\n\n```kotlin\nfun draw(canvas: Canvas, ...) {\n    // ...\n    drawSemaphore.tryAcquire()     // non-blocking，可能获取不到\n    val snapshot = engine.renderSnapshot()\n    releaseSemaphoreIfNeeded()     // 无条件 release\n    engine.draw(canvas, snapshot, config)\n}\n```\n\n`ActionHandler.MSG_FRAME_UPDATE` 使用 `drawSemaphore.acquire()`（阻塞），`draw()` 使用 `tryAcquire()`（非阻塞）。这个逻辑**意图是双缓冲同步**：main 线程读完 snapshot 后 release 让 action 线程继续写。但当前实现有一个微妙问题：`releaseSemaphoreIfNeeded()` 检查 `availablePermits() == 0` 才 release，但 `tryAcquire()` 如果失败了 permits 仍为 0，此时 `releaseSemaphoreIfNeeded()` 仍会 release —— 这在逻辑上是正确的（解阻 action 线程），但属于\"偶然正确\"，不够健壮。\n\n### 优化 2：Draw-time 位置插值（消除 pipeline 延迟）\n\n> 当前是信号量锁步同步的架构，这个修改没必要，不改\n\n`engine.draw()` 不再使用 snapshot 中预计算的 x 坐标，而是用当前帧的 `smoothPositionMs` 实时计算 SCROLL 弹幕的水平位置：\n\n```kotlin\nval x = if (item.kind == DanmakuKind.SCROLL)\n    scrollX(drawWidth, smoothPositionMs, item.startTimeMs, item.pxPerMs)\nelse snapshot.x[i]\n```\n\n**效果**：彻底消除 act→draw 之间 \\~16ms 的 pipeline 延迟，弹幕位置精确到当前 vsync 时刻。\n\n### 13. **[低等]** `RenderSnapshot` 未使用对象池\n\n**文件**：RenderSnapshot.kt\n\n当前双缓冲的 `snapshotA/B` 内部的 `items/x/yTop/textWidth` 数组会在 `ensureCapacity` 时创建新数组。高弹幕密度下频繁扩容会产生 GC 压力。当前实现已有 `cap = max(required, items.size * 2 + 8)` 的倍增策略，问题不大，但可以考虑在 init 时预分配一个合理大小（如 256）。\n\n### 14. `DanmakuTimer.EXTREME_DRIFT_REANCHOR_THRESHOLD_MS = 2000` 可能过于宽容\n\n如果播放器 buffering 2 秒后恢复，smooth position 最多偏移 2 秒。建议收紧到 `500`~`1000ms`。\n\n### 15. `loadDanmakuSegment` 中 `Color(it.color).toArgb()` 存在不必要的对象创建\n\n每条弹幕都创建一个 `androidx.compose.ui.graphics.Color` 对象再 `toArgb()`，可直接使用位运算 `0xFF000000.toInt() or (it.color and 0xFFFFFF)` 避免装箱。"
  },
  {
    "path": "doc/弹幕/弹幕库优化.md",
    "content": "## 变更总结\n\n### DanmakuItem.kt\n新增 `isActive` 标志位，用于标记弹幕是否处于活跃状态，避免通过遍历 active 列表来判断。\n\n### DanmakuEngine.kt — 三大改动方向：\n\n**1. 移除 pending 重试机制**\n- 删除了 `PendingSpawn` 数据类和 `pending: ArrayDeque` 队列\n- 删除了每帧重试 pending 弹幕的逻辑和相关常量（`DELAY_STEP_MS`、`MAX_DELAY_MS`、`MAX_PENDING`、`MAX_PENDING_RETRY_PER_FRAME`）\n- 现在放不下轨道的弹幕直接丢弃，不再排队重试\n\n**2. 轨道状态管理重构**\n- **滚动弹幕**：从每帧 `Arrays.fill` 重建 `laneLastScroll`/`laneLastScrollTail` → 改为持久化的 `scrollLaneQueues: Array<ArrayDeque<DanmakuItem>>`，只做增量清理（`cleanupScrollLaneQueue`），用 `isActive` 标记判断是否过期\n- **固定弹幕**：从存储 `laneLastTop`/`laneLastBottom`（引用对象后每帧扫描比较时间）→ 改为 `topLaneBusyUntilMs`/`bottomLaneBusyUntilMs`（`DoubleArray`），直接比较数值，无需引用对象\n\n**3. 快照发布 & 缓存构建优化**\n- 引入 `snapshotDirty` 脏标志，只在 active 列表变化时才重建快照（`publishSnapshotIfDirty`），而非每帧都重建\n- 快照不再存储 `x` 坐标，改为 `draw()` 时实时计算 `scrollX`，减少快照写入量\n- 缓存构建从每帧全量扫描 active → 改为 `cacheProbeCursor` 轮询探测，每帧最多扫描 `MAX_CACHE_SCAN_PER_FRAME=16` 项\n\n---\n\n**性能提升主要体现在：**\n\n| 优化点 | 效果 |\n|---|---|\n| 移除 pending 队列 | 消除每帧重试开销和内存压力，弹幕高峰期（如高能进度条处）不再因 260 条 pending 反复重试拖慢 action 线程 |\n| 轨道状态增量维护 | 旧实现每帧对所有轨道 `Arrays.fill` + 遍历全部 active 重建轨道映射，$O(active)$；新实现只做队列头尾的过期清理，通常为 $O(1)$ |\n| 固定弹幕轨道用 `DoubleArray` | 减少对象引用和比较开销，CPU cache 更友好 |\n| `snapshotDirty` 脏标志 | 无新增/移除弹幕的帧跳过整个快照构建，减少对象拷贝和数组操作 |\n| 缓存构建轮询探测 | 从每帧 $O(active)$ 全量扫描降为最多 16 项，弹幕密集时显著降低每帧耗时 |\n| draw 时实时算 x | 快照写入少一个 float 数组，减少 action→UI 线程的数据传输量 |\n\n**潜在取舍**：放弃 pending 意味着轨道满时弹幕会被直接丢弃，弹幕密集场景下显示数量可能略少，但换来了更稳定的帧率。整体来看是合理的性能优化。\n\n\n\n# Plan: DanmakuView 绘制路径优化\n\n消除不必要的硬件层开销 + 缓存预热消除 drawTextDirect fallback。不涉及架构重构，改动集中在 DanmakuView 和 DanmakuEngine。\n\n## Steps\n\n### Phase 1: 消除无条件 LAYER_TYPE_HARDWARE（主要优化）\n\n1. **问题**：DanmakuView.kt L27 `init { setLayerType(LAYER_TYPE_HARDWARE, null) }` — 始终开启硬件层。硬件层的设计目的是给**静态内容 + 变换动画**用的（比如 alpha/translate 动画）。弹幕内容每帧都变（invalidate），硬件层反而**每帧要重新渲染整个层纹理再合成**，比 `LAYER_TYPE_NONE`（直接绘制到 display list）多一次完整的纹理拷贝。\n\n2. **修改**：只在有蒙版时使用硬件层（DstIn 混合需要离屏 buffer），无蒙版时用 `LAYER_TYPE_NONE`。在 `onDraw()` 中根据 `maskFrame` 是否为 null 动态切换。\n\n3. *依赖*：无。可独立实施。\n\n### Phase 3（可选）: 更轻量的 fallback\n\n7. **修改**：当 `cacheBitmap == null` 时，完全跳过该弹幕直到 cache 就绪（下一帧补画）。\n\n## Relevant files\n- `player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/DanmakuView.kt` — 修改 `init` 硬件层设置，只在有蒙版时使用硬件层\n- `player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/DanmakuEngine.kt` — 删除 `drawTextDirect` fallback，改为等待 cache 就绪\n\n## Verification\n1. 打开 `DanmakuLogStats.logEnabled = true`，对比修改前后：`[Draw] fps`, `dropped`, `snapAgeMs` 指标\n2. 无蒙版场景：确认 `layerType == LAYER_TYPE_NONE`\n3. 有蒙版场景：确认 DstIn 混合效果不变\n4. 密集弹幕场景（seek 到高密度时间点）：观察 `drawTextDirect` 是否被触发（可加临时计数日志）\n5. GPU Profiler（`adb shell dumpsys gfxinfo`）对比每帧 GPU 耗时\n\n## Decisions\n- 三缓冲非阻塞已排除（用户实测无改善，且破坏流程）\n- SurfaceView/TextureView 不采用（ROI 过低）\n- 运动模糊不采用（文字不适合）\n- Phase 1 是最高优先级，因为它消除了已知的每帧额外 GPU 拷贝\n\n## Further Considerations\n1. **120Hz 支持**：Choreographer 在 120Hz 设备上天然按 120Hz 回调。当前代码的 Semaphore 握手只要 Action+Draw 在 8.3ms 内完成就能维持 120fps。如果目标设备有 120Hz（部分手机），可以验证是否自然支持。\n2. **Phase 2 前瞻窗口大小**：前瞻多少条待 spawn 弹幕？建议 `MAX_PREFETCH = 24`（约 2 帧的 spawn 量），如果缓存队列未满就提前提交。\n"
  },
  {
    "path": "doc/弹幕/弹幕重构需求.md",
    "content": "## 弹幕系统重写需求文档 v4（最终版）\n\n### 一、目标\n\n弃用 akdanmaku 模块，重写为纯 Kotlin + Canvas 弹幕引擎，专用于本项目。参考 [cat3399/blbl danmaku](D:\\code\\blbl\\app\\src\\main\\java\\blbl\\cat3399\\feature\\player\\danmaku)。\n\n---\n\n### 二、弹幕类型与布局\n\n#### 2.1 滚动弹幕 (mode=1)\n- 从右向左匀速滚动\n- **速度 = 基础速度 × 播放速率 × speedLevel 系数**\n  - 基础速度由弹幕飞越屏幕的标准时长决定（约 6 秒）\n  - 播放速率：跟随视频变速（1.0x / 1.5x / 2.0x 等），弹幕同步加减速\n  - speedLevel（1~10 档）：用户手动叠加的速度偏好，独立于播放速率\n- **轨道间距**：同轨道新弹幕必须等前一条完全进入屏幕并留出 `marginPx`（约 0.6 × 文字盒高度）后才可 spawn。所有轨道满时进入 pending 队列延迟重试\n- **追赶允许**：spawn 后快速弹幕追上前方慢速弹幕产生视觉重叠是正常行为，不做碰撞检测\n- 长文本弹幕限速：`pxPerMs` 上限为短文本速度的 1.5 倍\n\n#### 2.2 顶部弹幕 (mode=5) / 底部弹幕 (mode=4)\n- **水平居中**显示，固定 4 秒\n- 逐轨道堆叠，同轨道前一条到期后才允许后续弹幕进入\n\n#### 2.3 字体大小与轨道\n\n- **轨道高度**由基准行高决定：`textSizeSp × (textSizeScale / 100)`\n- 每条弹幕有 API `textSize`，实际渲染字号 = `min(danmaku.textSize, 25) × (textSizeScale / 100)`（25 为标准字号上限，超过的裁剪到标准）\n- 小字号弹幕在轨道内**垂直居中**对齐\n- `textSizeScale` 为百分比整数，范围 **25 ~ 200**（表示 25%~200%）\n\n---\n\n### 三、数据模型\n\n```kotlin\ndata class Danmaku(\n    val dmid: Long,\n    val positionMs: Int,    // 出现时间（毫秒）\n    val text: String,      // 弹幕文本（可含 Unicode emoji 和 [token] 表情）\n    val mode: Int,         // 1=Scroll, 4=Bottom, 5=Top\n    val textSize: Int,     // API 原始字号\n    val color: Int,        // ARGB 颜色\n    val level: Int = 0,    // 用户等级（过滤用）\n)\n```\n\n---\n\n### 四、数据操作\n\n- `setDanmakus(list)` — 替换全部数据\n- `appendDanmakus(list, maxItems, alreadySorted)` — 追加数据并限制总条数\n- `trimToTimeRange(min, max)` — 裁剪时间范围外数据\n- `notifySeek(positionMs)` — 跳转并清屏\n\n---\n\n### 五、播放同步\n\n通过 Provider 函数每帧查询：\n- `positionProvider: () -> Long`\n- `isPlayingProvider: () -> Boolean`\n- `playbackSpeedProvider: () -> Float`\n\n**DanmakuTimer** 时间平滑：\n- `System.nanoTime()` 驱动单调时钟，消除 position 抖动\n- Seek / 暂停恢复 / 变速切换时重新锚定\n- 极端漂移（>2s）自动修正\n\n暂停时冻结弹幕位置，恢复时继续。\n\n---\n\n### 六、配置\n\n```kotlin\ndata class DanmakuConfig(\n    val enabled: Boolean,\n    val opacity: Float,                // 0.1~1.0\n    val textSizeSp: Float,             // 基准字体大小 (SP)\n    val textSizeScale: Int,            // 百分比 25~200 (25%~200%)\n    val fontWeight: DanmakuFontWeight, // Normal / Bold\n    val strokeWidthPx: Int,            // 描边宽度\n    val speedLevel: Int,               // 用户滚动速度偏好 1~10\n    val area: Float,                   // 显示区域占比 0.0~1.0\n    val laneDensity: DanmakuLaneDensity, // Sparse / Standard / Dense\n    // 过滤\n    val allowScroll: Boolean = true,\n    val allowTop: Boolean = true,\n    val allowBottom: Boolean = true,\n    val minLevel: Int = 0,\n)\n```\n\n滚动速度最终公式：`pxPerMs = basePxPerMs × playbackSpeed × speedMultiplier(speedLevel)`\n\n---\n\n### 七、过滤\n\n引擎内部维护双份数据：`allItems`（全量原始数据）+ `items`（过滤后数据）。\n\n- `setDanmakus` / `appendDanmakus` 同时写入 `allItems`，并按当前过滤条件生成 `items`\n- 帧循环只操作 `items`，不做任何过滤判断\n- Config 过滤字段（`allowScroll/Top/Bottom` / `minLevel`）变更时：从 `allItems` 重建 `items` + `seekTo(currentPosition)`\n- 过滤条件稳定后，帧循环零过滤开销\n\n---\n\n### 八、Emoji 支持\n\n- **Unicode emoji**（😀🎉 等）：`Canvas.drawText()` 原生支持\n- **B站自定义表情**（`[doge]` 等 `[token]` 语法）：解析 → 查询表情 URL → 异步加载图片 → inline segment 绘制。未就绪时画占位方块，就绪后重新构建缓存\n\n---\n\n### 九、渲染管线\n\n```\n┌──────────────────────────────────────────────────┐\n│  CacheThread (HandlerThread, 低优先级)            │\n│  Bitmap 构建请求 → 绘制文字+emoji 到 Bitmap       │\n│  → 设置 item.cacheBitmap (volatile)              │\n│  → post 主线程 invalidate                        │\n└──────────────┬───────────────────────────────────┘\n               │\n┌──────────────▼───────────────────────────────────┐\n│  ActionThread (HandlerThread)                     │\n│  Choreographer callback → Semaphore 等 draw 完成  │\n│  → act():                                         │\n│    1) pruneExpired                                │\n│    2) skipOld / dropIfLagging                     │\n│    3) retryPending                                │\n│    4) spawnNew（轨道间距检查）                      │\n│    5) requestCacheBuilds → CacheThread            │\n│    6) 写 RenderSnapshot (双缓冲 A/B)              │\n│  → View.invalidateOnAnimation                     │\n└──────────────┬───────────────────────────────────┘\n               │ latestSnapshot (volatile)\n┌──────────────▼───────────────────────────────────┐\n│  Main Thread — View.onDraw(canvas)                │\n│    1) 读 Provider (position/config/playing)       │\n│    2) DanmakuTimer.step() 平滑时间                │\n│    3) stepTime + drainReleasedBitmaps             │\n│    4) 获取 snapshot → release Semaphore            │\n│    5) canvas.clipPath(maskPath)（如有蒙版）        │\n│    6) 遍历 snapshot:                              │\n│       有 cacheBitmap → drawBitmap                 │\n│       无缓存 → drawText fallback                  │\n│    7) postInvalidateOnAnimation（局部刷新）        │\n└──────────────────────────────────────────────────┘\n```\n\n---\n\n### 十、蒙版\n\n在 DanmakuView 的 onDraw 中处理：\n- `setMaskFrame(frame)` 更新蒙版帧（volatile）\n- onDraw 中 `canvas.save() → clipPath(maskPath) → 绘制弹幕 → restore()`\n- SVG Path 解析结果缓存为 Android `Path` 对象，同一 maskFrame 不重复解析\n\n---\n\n### 十一、缓存系统 (CacheManager)\n\n- **BitmapPool**：最大 50MB / 72 个 Bitmap，尺寸近似复用（宽差 ≤48px，高差 ≤24px）\n- **生成号**：Config 变更（字号/描边/字体）递增 `cacheStyleGeneration`，旧缓存惰性失效\n- **流控**：每帧最多 8 个缓存请求，队列深度上限 48\n\n---\n\n### 十二、性能约束\n\n| 参数 | 值 |\n|------|----|\n| MAX_SPAWN_PER_FRAME | 48 |\n| MAX_PENDING | 260 |\n| MAX_DELAY_MS | 1600ms |\n| MAX_CATCH_UP_LAG_MS | 1200ms |\n| MAX_CACHE_REQUESTS_PER_FRAME | 8 |\n| MAX_CACHE_QUEUE_DEPTH | 48 |\n| FIXED_DURATION_MS | 4000ms |\n| BITMAP_POOL_MAX_BYTES | 50MB |\n| BITMAP_POOL_MAX_COUNT | 72 |\n\n---\n\n### 十三、直播弹幕策略\n\n- `appendDanmakus(list, maxItems=5000)` 追加并限制总条数\n- 超出上限时丢弃最旧数据\n- 过期弹幕每帧自动从 active 列表清除\n\n---\n\n### 十四、DanmakuView (extends View)\n\n```kotlin\nclass DanmakuView(context: Context) : View(context) {\n    fun setPositionProvider(provider: () -> Long)\n    fun setIsPlayingProvider(provider: () -> Boolean)\n    fun setPlaybackSpeedProvider(provider: () -> Float)\n    fun setConfigProvider(provider: () -> DanmakuConfig)\n\n    fun setDanmakus(list: List<Danmaku>)\n    fun appendDanmakus(list: List<Danmaku>, maxItems: Int = 0, alreadySorted: Boolean = false)\n    fun trimToTimeRange(minPositionMs: Long, maxPositionMs: Long)\n    fun notifySeek(positionMs: Long)\n\n    fun setMaskFrame(frame: DanmakuMaskFrame?)\n    fun setVideoAspectRatio(ratio: Float)\n}\n```\n\nView 始终 `fillMaxSize()`，区域/透明度/蒙版全部在 onDraw 内部处理。\n\n---\n\n### 十五、移除 DanmakuLayer\n\n删除 `DanmakuLayer.kt` 和 `DanmakuLayerHandle`。在 BvPlayer 中直接使用：\n\n```kotlin\nAndroidView(\n    factory = { ctx -> DanmakuView(ctx).also { bindProviders(it) } },\n    modifier = Modifier.fillMaxSize()\n)\n```\n\n---\n\n### 十六、模块结构\n\n```\nplayer/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/\n├── DanmakuView.kt\n├── DanmakuPlayer.kt\n├── DanmakuEngine.kt\n├── DanmakuTimer.kt\n├── DanmakuConfig.kt\n├── CacheManager.kt\n└── model/\n    ├── Danmaku.kt\n    ├── DanmakuItem.kt\n    ├── DanmakuKind.kt\n    ├── RenderSnapshot.kt\n    └── DanmakuInlineSegment.kt\n```\n\n不创建独立 module，作为 shared 内部包。\n\n---\n\n### 十七、调用方变更\n\n| 文件 | 变更 |\n|------|------|\n| `VideoPlayerV3ViewModel` | `DanmakuItemData` → 新 `Danmaku`；移除旧 DanmakuPlayer 创建 |\n| `BvPlayer (TV)` | 移除 `TypeFilter`/`LevelFilter`；过滤内聚到新 `DanmakuConfig`；移除 `DanmakuLayer`，改用 `AndroidView { DanmakuView }` |\n| `BvPlayer (Mobile)` | 同上 |\n| `DanmakuLayer.kt` | **删除** |\n| `DanmakuMenu (TV/Mobile)` | 适配新 DanmakuConfig 字段 |\n| `build.gradle` 文件 | 移除 akdanmaku 依赖 |\n\n---\n\n### 十八、不实现\n\n- 发送弹幕\n- 弹幕合并/去重\n- 用户屏蔽\n- 自定义渲染器接口\n- SurfaceView / TextureView\n\n---\n\n--------------------------------------------------------------------------------------\n\n# BV项目弹幕系统完整结构\n\n## 总体架构\n\n项目分为两层：\n1. **低层引擎**：akdanmaku 模块（外部库，快手开源）\n2. **高层封装**：player 模块（本项目特定集成）\n\n## akdanmaku 模块（99 Java/Kotlin 文件）\n\n位置：d:\\code\\bv\\akdanmaku\\src\\main\\java\\com\\kuaishou\\akdanmaku\\\n\n### 核心类\n- **DanmakuPlayer**：主弹幕播放器，管理引擎和数据系统\n- **DanmakuView**：Android View，用于显示弹幕（与 DanmakuPlayer 配对）\n- **DanmakuEngine**：ECS 架构核心引擎\n- **DanmakuConfig**：弹幕全局配置\n\n### 模块目录\n- **ui/**：DanmakuPlayer, DanmakuView, DanmakuListener, DanmakuDisplayer\n- **ecs/**：ECS 架构核心（DanmakuEngine, DanmakuContext）\n- **ecs/component/**：组件系统（过滤、布局、动作等）\n- **ecs/system/**：系统（DataSystem, LayoutSystem, RenderSystem, ActionSystem）\n- **data/**：数据结构（DanmakuItem, DanmakuItemData, DataSource）\n- **layout/**：布局器（Rolling, Top, Bottom, Collision 等）\n- **render/**：渲染（DanmakuRenderer, RenderObject, SimpleRenderer）\n- **cache/**：缓存管理（BitmapPool, DrawingCache 等）\n- **utils/**：工具（DanmakuTimer, ObjectPool, Families, DanmakuPerformanceMonitor）\n- **ext/**：扩展（DanmakuExt, EntityExt, EngineExt 等）\n\n## Player 模块 danmaku 相关文件\n\n### player/shared - 共享代码\n- **AkDanmakuPlayer.kt**：Compose 包装器，AndroidView + DanmakuPlayer\n- **entity/DanmakuType.kt**：弹幕类型枚举（All, Top, Rolling, Bottom）\n- **entity/VideoPlayerDanmakuMenuItem.kt**：弹幕菜单项（Switch, Size, Opacity, Area, Mask, FilterLevel）\n- **entity/VideoPlayerData.kt**：数据类（VideoPlayerConfigData, VideoPlayerSeekData 等）\n- **util/DanmakuMaskFinder.kt**：蒙版查找工具（二分查找）\n- **util/DanmakuMaskModifiers.kt**：蒙版 Modifier（web/svg, mob/binary）\n\n### player/tv - TV 特定实现\n- **BvPlayer.kt**：TV 版播放器 Compose 组件\n- **DanmakuLayer.kt**：弹幕层组件（DanmakuLayerHandle + Composable）\n- **controller/playermenu/DanmakuMenu.kt**：TV 弹幕菜单\n\n### player/mobile - Mobile 特定实现\n- **BvPlayer.kt**：Mobile 版播放器 Compose 组件\n- **controller/menu/DanmakuMenu.kt**：Mobile 弹幕菜单\n\n## VideoModel 层\n\n**app/shared/src/main/kotlin/dev/aaa1115910/bv/viewmodel/VideoPlayerV3ViewModel.kt**\n\n关键属性：\n- `danmakuPlayer: DanmakuPlayer?`\n- `danmakuMasks: List<DanmakuMaskSegment>`\n- `currentDanmakuScale`: 弹幕缩放\n- `currentDanmakuOpacity`: 不透明度\n- `currentDanmakuEnabled`: 是否启用\n- `currentDanmakuTypes`: 弹幕类型列表\n- `currentDanmakuArea`: 显示区域占比\n- `currentDanmakuMask`: 是否启用蒙版\n- `currentDanmakuRollingDurationFactor`: 滚动速度系数\n- `currentDanmakuFilterLevel`: 等级过滤\n\n## 关键集成点\n\n1. **BvPlayer** 初始化弹幕：\n   - 创建 DanmakuPlayer 实例\n   - 设置 TypeFilter, LevelFilter\n   - 绑定 DanmakuConfig\n\n2. **DanmakuLayer** 负责：\n   - 持有 DanmakuLayerHandle（稳定引用）\n   - 应用蒙版 Modifier\n   - 调用 AkDanmakuPlayer\n\n3. **DanmakuMenu** 提供交互：\n   - 滚动速度、大小、不透明度控制\n   - 类型过滤器、等级过滤\n   - 蒙版开关\n\n## akdanmaku 依赖\n- gdx (LibGdx) 1.14.0\n- ashley (ECS) 1.7.4\n\n---\n\n## 新 BLBL 弹幕系统（参考实现）\n\n位置：d:\\code\\blbl\\app\\src\\main\\java\\blbl\\cat3399\\feature\\player\\danmaku\\\n\n### 架构特点\n- **无 ECS**：直接命令式设计，更轻量\n- **三线程**：Main（绘制）/ ActionThread（每帧逻辑）/ CacheThread（异步缓存构建）\n- **双缓冲快照**：RenderSnapshot 隔离计算和绘制\n- **Choreographer 驱动**：vsync 同步，精确帧控制\n\n### 关键文件\n1. **DanmakuView.kt**（550行）：public API，View 包装\n2. **DanmakuPlayer.kt**（470行）：三线程调度器、Choreographer 管理\n3. **DanmakuEngine.kt**（700+行）：核心弹幕逻辑（spawn、layout、render）\n4. **CacheManager.kt**（500+行）：Bitmap 缓存、线程管理、内存池\n5. **DanmakuTimer.kt**（100行）：System.nanoTime() 驱动、平滑时间\n6. **DanmakuModels.kt**：配置类（DanmakuConfig、DanmakuSessionSettings）\n7. **model/**\n   - **DanmakuItem.kt**：包装 Danmaku + 缓存状态\n   - **RenderSnapshot.kt**：双缓冲快照\n   - **DanmakuInlineSegment.kt**：文字/emoji/高赞图标混合渲染\n\n### 性能指标\n- MAX_SPAWN_PER_FRAME: 48\n- MAX_PENDING: 260\n- MAX_CACHE_REQUESTS_PER_FRAME: 8\n- BITMAP_POOL: 50MB / 72 个\n- MAX_DELAY_MS: 1600ms\n- MAX_CATCH_UP_LAG_MS: 1200ms\n\n### 关键特性\n1. **轨道管理**：滚动/顶部/底部分离，每轨道前后间距检查\n2. **速度叠加**：baseSpeed × playbackSpeed × speedLevel 系数（图表约 1.5 倍）\n3. **Pending 队列**：轨道满时入队延迟重试，超时丢弃\n4. **异步缓存**：CacheThread 构建 Bitmap，volatile 发布到主线程\n5. **Inline 段**：Unicode emoji + B站自定义表情 + 高赞图标\n6. **蒙版**：onDraw 时 canvas.clipPath()，SVG Path 缓存\n7. **流控**：缓存请求每帧限 8 个，队列深度限 48，防止堆积\n8. **平滑时间**：System.nanoTime() 消除 position 抖动（seek/暂停恢复/变速时重新锚定）\n\n### 数据模型\n```kotlin\ndata class Danmaku(\n    val dmid: Long, positionMs: Int, text: String,\n    val mode: Int,  // 1=Scroll/4=Bottom/5=Top\n    val textSize: Int,  // API 原始字号\n    val color: Int,  // ARGB\n    val level: Int = 0  // 用户等级（过滤用）\n)\n```\n\n### 渲染管线\n```\nCacheThread（HandlerThread）\n  ↓ 构建 Bitmap （文字+emoji）→ item.cacheBitmap\n  ↓ 发布 invalidate\n\nActionThread（HandlerThread）\n  ↓ Choreographer → act()\n  ↓ pruneExpired, retryPending, spawnNew, requestCacheBuilds\n  ↓ 写 RenderSnapshot（双缓冲 A/B）→ latestSnapshot\n\nMain Thread\n  ↓ onDraw(canvas)\n  ↓ DanmakuTimer.step() 平滑时间\n  ↓ 读 snapshot，调用 release() 唤醒 ActionThread\n  ↓ 逐项: cacheBitmap ? drawBitmap() : drawText fallback\n```\n"
  },
  {
    "path": "doc/弹幕/重构后.txt",
    "content": "---------------------------- PROCESS STARTED (28950) for package dev.aaa1115910.bv2.debug ----------------------------\n2026-04-09 09:01:27.260 28950-29028 System.out              dev.aaa1115910.bv2.debug             I  getVideoDetail:{\"code\":0,\"message\":\"OK\",\"ttl\":1,\"data\":{\"View\":{\"bvid\":\"BV1QmkLY5ESw\",\"aid\":113666158495041,\"videos\":1,\"tid\":183,\"tid_v2\":2002,\"tname\":\"\",\"tname_v2\":\"\",\"copyright\":1,\"pic\":\"http://i2.hdslb.com/bfs/archive/1853161b265b44ee7a5af81695c3b0177720b4ea.jpg\",\"title\":\"“质问唐僧的那一刻，艺术达到了顶峰！”\",\"pubdate\":1734408044,\"ctime\":1734408044,\"desc\":\"感谢审核大大！！！\",\"desc_v2\":[{\"raw_text\":\"感谢审核大大！！！\",\"type\":1,\"biz_id\":0}],\"state\":0,\"duration\":256,\"mission_id\":4023025,\"rights\":{\"bp\":0,\"elec\":0,\"download\":1,\"movie\":0,\"pay\":0,\"hd5\":1,\"no_reprint\":1,\"autoplay\":1,\"ugc_pay\":0,\"is_cooperation\":0,\"ugc_pay_preview\":0,\"no_background\":0,\"clean_mode\":0,\"is_stein_gate\":0,\"is_360\":0,\"no_share\":0,\"arc_pay\":0,\"free_watch\":0},\"owner\":{\"mid\":111578972,\"name\":\"前前前世的陌生人\",\"face\":\"https://i2.hdslb.com/bfs/face/caa531d35eb71a06403da433b0549fe623675dfd.jpg\"},\"stat\":{\"aid\":113666158495041,\"view\":10351058,\"danmaku\":113435,\"reply\":6791,\"favorite\":181390,\"coin\":97018,\"share\":11202,\"now_rank\":0,\"his_rank\":85,\"like\":409477,\"dislike\":0,\"evaluation\":\"\",\"vt\":0},\"argue_info\":{\"argue_msg\":\"\",\"argue_type\":0,\"argue_link\":\"\"},\"dynamic\":\"“质问唐僧的那一刻，艺术达到了顶峰”\",\"cid\":27381926762,\"dimension\":{\"width\":3840,\"height\":2160,\"rotate\":0},\"premiere\":null,\"teenage_mode\":0,\"is_chargeable_season\":false,\"is_story\":false,\"is_upower_exclusive\":false,\"is_upower_play\":false,\"is_upower_preview\":false,\"enable_vt\":0,\"vt_display\":\"\",\"is_upower_exclusive_with_qa\":false,\"no_cache\":false,\"pages\":[{\"cid\":27381926762,\"page\":1,\"from\":\"vupload\",\"part\":\"“质问唐僧的那一刻，艺术达到了顶峰！”\",\"duration\":256,\"vid\":\"\",\"weblink\":\"\",\"dimension\":{\"width\":3840,\"height\":2160,\"rotate\":0},\"first_frame\":\"http://i2.hdslb.com/bfs/storyff/n241217sa3avhp4hfd8moj3odibfm9p5_firsti.jpg\",\"ctime\":1734408044}],\"subtitle\":{\"allow_submit\":false,\"list\":[{\"id\":1646328479605701376,\"lan\":\"ai-zh\",\"lan_doc\":\"中文\",\"is_lock\":false,\"subtitle_url\":\"\",\"type\":1,\"id_str\":\"1646328479605701376\",\"ai_type\":0,\"ai_status\":2,\"subtitle_height\":null,\"author\":{\"mid\":0,\"name\":\"\",\"sex\":\"\",\"face\":\"\",\"sign\":\"\",\"rank\":0,\"birthday\":0,\"is_fake_account\":0,\"is_deleted\":0,\"in_reg_audit\":0,\"is_senior_member\":0,\"name_render\":null,\"handle\":\"\"}}]},\"is_season_display\":false,\"user_garb\":{\"url_image_ani_cut\":\"\"},\"honor_reply\":{\"honor\":[{\"aid\":113666158495041,\"type\":3,\"desc\":\"全站排行榜最高第85名\",\"weekly_recommend_num\":0},{\"aid\":113666158495041,\"type\":7,\"desc\":\"热门收录\",\"weekly_recommend_num\":0}]},\"like_icon\":\"\",\"need_jump_bv\":false,\"disable_show_up_info\":false,\"is_story_play\":1,\"is_view_self\":false},\"Card\":{\"card\":{\"mid\":\"111578972\",\"name\":\"前前前世的陌生人\",\"approve\":false,\"sex\":\"男\",\"rank\":\"10000\",\"face\":\"https://i2.hdslb.com/bfs/face/caa531d35eb71a06403da433b0549fe623675dfd.jpg\",\"face_nft\":0,\"face_nft_type\":0,\"DisplayRank\":\"0\",\"regtime\":0,\"spacesta\":0,\"birthday\":\"\",\"place\":\"\",\"description\":\"\",\"article\":0,\"attentions\":[],\"fans\":417849,\"friend\":344,\"attention\":344,\"sign\":\"感谢遇见交流群号：1142539222，先进群，在拉V\",\"level_info\":{\"current_level\":6,\"current_min\":0,\"current_exp\":0,\"next_exp\":0},\"pendant\":{\"pid\":0,\"name\":\"\",\"image\":\"\",\"expire\":0,\"image_enhance\":\"\",\"image_enhance_frame\":\"\",\"n_pid\":0},\"nameplate\":{\"nid\":1,\"name\":\"黄金殿堂\",\"image\":\"https://i0.hdslb.com/bfs/face/82896ff40fcb4e7c7259cb98056975830cb55695.png\",\"image_small\":\"https://i1.hdslb.com/bfs/face/627e342851dfda6fe7380c2fa0cbd7fae2e61533.png\",\"level\":\"稀有勋章\",\"condition\":\"单个自制视频总播放数\\u003e=100万，数据次日更新\"},\"Official\":{\"role\":7,\"title\":\"bilibili 新星UP主\",\"desc\":\"“2020新星计划暑假赛”获奖者\",\"type\":0},\"official_verify\":{\"type\":0,\"desc\":\"bilibili 新星UP主\"},\"vip\":{\"type\":2,\"status\":1,\"due_date\":1806854400000,\"vip_pay_type\":0,\"theme_type\":0,\"label\":{\"path\":\"http://i0.hdslb.com/bfs/vip/label_annual.png\",\"text\":\"年度大会员\",\"label_theme\":\"annual_vip\",\"text_color\":\"#FFFFFF\",\"bg_style\":1,\"bg_color\":\"#FB7299\",\"border_color\":\"\n2026-04-09 09:01:27.975 28950-28979 VideoPlayerV3ViewModel  dev.aaa1115910.bv2.debug             I  ensureDanmakuView: current=dev.aaa1115910.bv.player.danmaku.DanmakuView{2613588 V.ED..... ......ID 0,0-1920,1080}\n2026-04-09 09:01:28.779 28950-28950 BvPlayer                dev.aaa1115910.bv2.debug             I  Sync danmaku config: DanmakuConfig(enabled=true, opacity=1.0, textSizeSp=18.0, textSizeScale=125, fontWeight=Bold, strokeWidthPx=4, speedLevel=3, area=1.0, laneDensity=Standard, allowScroll=true, allowTop=true, allowBottom=true, minLevel=0)\n2026-04-09 09:01:28.821 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=4.9  frames=5  dropped=3  actMs(avg/p50/p95/max)=0.48/0.05/2.19/2.19  active=0  pending=0  cacheQ=0  mem=heap=48.33/512.00MB native=149.18MB\n2026-04-09 09:01:28.840 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=4.9  frames=5  dropped=3  sameSnap=3  maxSameSnapStreak=3  snapAgeMs(avg/max)=9.6/33.8  mem=heap=50.15/512.00MB native=149.18MB\n2026-04-09 09:01:28.937 28950-29008 VideoPlayerV3ViewModel  dev.aaa1115910.bv2.debug             I  Load danmaku segment success, cid=27381926762, segment=1, size=9063, total=9063\n2026-04-09 09:01:29.824 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.8  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.14/0.04/0.55/1.18  active=28  pending=8  cacheQ=0  mem=heap=69.39/512.00MB native=150.20MB\n2026-04-09 09:01:29.841 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.9  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=18.5/96.0  mem=heap=73.35/512.00MB native=150.19MB\n2026-04-09 09:01:30.189 28950-29008 VideoPlayerV3ViewModel  dev.aaa1115910.bv2.debug             I  Load danmaku mask size: 26\n2026-04-09 09:01:30.825 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.04/0.27/0.43  active=46  pending=12  cacheQ=0  mem=heap=71.77/512.00MB native=151.05MB\n2026-04-09 09:01:30.859 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/22.9  mem=heap=72.05/512.00MB native=150.96MB\n2026-04-09 09:01:31.838 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.2  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.07/0.04/0.19/0.48  active=61  pending=9  cacheQ=1  mem=heap=75.56/512.00MB native=150.84MB\n2026-04-09 09:01:31.872 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.3  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/19.6  mem=heap=72.26/512.00MB native=150.82MB\n2026-04-09 09:01:32.844 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.04/0.33/0.94  active=77  pending=10  cacheQ=0  mem=heap=73.93/512.00MB native=150.49MB\n2026-04-09 09:01:32.873 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/22.9  mem=heap=74.21/512.00MB native=150.44MB\n2026-04-09 09:01:33.856 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.3  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.04/0.19/1.79  active=87  pending=2  cacheQ=0  mem=heap=77.57/512.00MB native=150.30MB\n2026-04-09 09:01:33.892 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/22.3  mem=heap=77.88/512.00MB native=150.42MB\n2026-04-09 09:01:34.865 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.5  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.04/0.30/0.37  active=94  pending=8  cacheQ=0  mem=heap=78.06/512.00MB native=150.45MB\n2026-04-09 09:01:34.901 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.5  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/26.7  mem=heap=78.43/512.00MB native=150.55MB\n2026-04-09 09:01:35.868 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.25/0.34  active=103  pending=17  cacheQ=2  mem=heap=78.21/512.00MB native=150.43MB\n2026-04-09 09:01:35.903 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/25.0  mem=heap=78.55/512.00MB native=150.46MB\n2026-04-09 09:01:36.871 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.21/0.31  active=95  pending=12  cacheQ=0  mem=heap=77.37/512.00MB native=150.00MB\n2026-04-09 09:01:36.905 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/23.1  mem=heap=77.65/512.00MB native=149.86MB\n2026-04-09 09:01:37.872 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.06/0.18/0.25  active=97  pending=11  cacheQ=0  mem=heap=76.87/512.00MB native=151.36MB\n2026-04-09 09:01:37.906 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/19.5  mem=heap=77.16/512.00MB native=151.22MB\n2026-04-09 09:01:38.877 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.12/0.06/0.45/1.64  active=99  pending=16  cacheQ=1  mem=heap=80.33/512.00MB native=151.21MB\n2026-04-09 09:01:38.910 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.3  mem=heap=80.33/512.00MB native=151.11MB\n2026-04-09 09:01:39.891 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.2  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.10/0.06/0.26/0.84  active=103  pending=20  cacheQ=0  mem=heap=78.08/512.00MB native=151.17MB\n2026-04-09 09:01:39.924 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.2  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/24.4  mem=heap=78.09/512.00MB native=151.13MB\n2026-04-09 09:01:40.896 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.12/0.05/0.38/1.26  active=105  pending=17  cacheQ=0  mem=heap=77.75/512.00MB native=150.32MB\n2026-04-09 09:01:40.933 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.5  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/21.8  mem=heap=78.31/512.00MB native=150.40MB\n2026-04-09 09:01:41.905 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.5  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.23/0.28  active=104  pending=19  cacheQ=0  mem=heap=81.02/512.00MB native=150.55MB\n2026-04-09 09:01:41.937 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/21.7  mem=heap=81.32/512.00MB native=150.42MB\n2026-04-09 09:01:42.908 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.21/0.28  active=102  pending=22  cacheQ=0  mem=heap=80.22/512.00MB native=150.93MB\n2026-04-09 09:01:42.944 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.6  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/25.1  mem=heap=80.51/512.00MB native=151.00MB\n2026-04-09 09:01:43.911 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.24/0.38  active=108  pending=9  cacheQ=0  mem=heap=79.54/512.00MB native=150.50MB\n2026-04-09 09:01:43.945 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.0  mem=heap=79.82/512.00MB native=150.45MB\n2026-04-09 09:01:44.913 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.26/0.49  active=107  pending=11  cacheQ=0  mem=heap=81.64/512.00MB native=150.62MB\n2026-04-09 09:01:44.949 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/23.1  mem=heap=81.92/512.00MB native=150.49MB\n2026-04-09 09:01:45.919 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.6  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.23/0.41  active=105  pending=8  cacheQ=0  mem=heap=81.56/512.00MB native=150.41MB\n2026-04-09 09:01:45.955 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.6  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/21.1  mem=heap=81.84/512.00MB native=150.36MB\n2026-04-09 09:01:46.930 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.3  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.07/0.05/0.24/0.45  active=106  pending=1  cacheQ=0  mem=heap=81.35/512.00MB native=150.26MB\n2026-04-09 09:01:46.965 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.5  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.4/20.0  mem=heap=81.63/512.00MB native=150.25MB\n2026-04-09 09:01:47.935 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.23/0.70  active=99  pending=3  cacheQ=0  mem=heap=79.47/512.00MB native=150.23MB\n2026-04-09 09:01:47.968 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/24.0  mem=heap=79.75/512.00MB native=150.39MB\n2026-04-09 09:01:48.952 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.07/0.05/0.16/0.18  active=96  pending=2  cacheQ=0  mem=heap=82.29/512.00MB native=150.50MB\n2026-04-09 09:01:48.968 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/23.0  mem=heap=82.29/512.00MB native=150.48MB\n2026-04-09 09:01:49.967 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.32/0.49  active=89  pending=8  cacheQ=0  mem=heap=81.81/512.00MB native=149.73MB\n2026-04-09 09:01:49.985 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/19.7  mem=heap=81.81/512.00MB native=149.69MB\n2026-04-09 09:01:50.971 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.07/0.05/0.17/0.24  active=93  pending=4  cacheQ=0  mem=heap=81.08/512.00MB native=150.09MB\n2026-04-09 09:01:50.986 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/22.7  mem=heap=81.08/512.00MB native=150.09MB\n2026-04-09 09:01:51.985 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.2  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.20/0.49  active=91  pending=11  cacheQ=0  mem=heap=80.63/512.00MB native=149.64MB\n2026-04-09 09:01:52.001 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/24.2  mem=heap=80.64/512.00MB native=149.71MB\n2026-04-09 09:01:52.990 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.04/0.17/1.34  active=92  pending=9  cacheQ=0  mem=heap=79.60/512.00MB native=150.09MB\n2026-04-09 09:01:53.008 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.6  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/24.8  mem=heap=79.89/512.00MB native=150.03MB\n2026-04-09 09:01:53.992 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.07/0.06/0.16/0.20  active=94  pending=9  cacheQ=0  mem=heap=82.84/512.00MB native=150.42MB\n2026-04-09 09:01:54.011 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.5  mem=heap=82.85/512.00MB native=150.42MB\n2026-04-09 09:01:54.999 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.6  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.07/0.04/0.19/0.29  active=97  pending=8  cacheQ=1  mem=heap=82.52/512.00MB native=150.18MB\n2026-04-09 09:01:55.016 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.7  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/21.6  mem=heap=83.10/512.00MB native=150.18MB\n2026-04-09 09:01:56.002 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.33/0.53  active=91  pending=5  cacheQ=0  mem=heap=82.21/512.00MB native=150.07MB\n2026-04-09 09:01:56.019 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/21.9  mem=heap=82.22/512.00MB native=149.98MB\n2026-04-09 09:01:57.003 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.07/0.05/0.20/0.24  active=93  pending=10  cacheQ=1  mem=heap=84.90/512.00MB native=150.06MB\n2026-04-09 09:01:57.020 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/21.4  mem=heap=80.66/512.00MB native=150.01MB\n2026-04-09 09:01:58.006 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.06/0.04/0.18/0.21  active=94  pending=8  cacheQ=0  mem=heap=82.99/512.00MB native=149.88MB\n2026-04-09 09:01:58.022 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.4  mem=heap=83.00/512.00MB native=149.88MB\n2026-04-09 09:01:59.022 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.07/0.05/0.15/0.19  active=91  pending=13  cacheQ=0  mem=heap=80.65/512.00MB native=149.75MB\n2026-04-09 09:01:59.041 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/22.8  mem=heap=80.66/512.00MB native=149.75MB\n2026-04-09 09:02:00.029 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.6  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.18/0.75  active=96  pending=14  cacheQ=0  mem=heap=84.01/512.00MB native=150.48MB\n2026-04-09 09:02:00.047 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.7  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/22.4  mem=heap=84.28/512.00MB native=150.37MB\n2026-04-09 09:02:01.035 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.24/0.52  active=99  pending=16  cacheQ=0  mem=heap=82.80/512.00MB native=150.11MB\n2026-04-09 09:02:01.051 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/21.0  mem=heap=82.80/512.00MB native=150.04MB\n2026-04-09 09:02:02.049 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.2  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.17/0.32  active=100  pending=15  cacheQ=0  mem=heap=82.14/512.00MB native=150.50MB\n2026-04-09 09:02:02.065 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.2  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/20.5  mem=heap=82.46/512.00MB native=150.54MB\n2026-04-09 09:02:03.053 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.13/0.07/0.36/1.27  active=106  pending=14  cacheQ=0  mem=heap=85.12/512.00MB native=150.15MB\n2026-04-09 09:02:03.082 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/25.5  mem=heap=81.22/512.00MB native=150.18MB\n2026-04-09 09:02:04.066 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.3  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.23/0.45  active=115  pending=22  cacheQ=0  mem=heap=84.33/512.00MB native=149.98MB\n2026-04-09 09:02:04.084 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.2  mem=heap=84.63/512.00MB native=149.94MB\n2026-04-09 09:02:05.074 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.5  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.07/0.19/0.20  active=115  pending=26  cacheQ=0  mem=heap=84.63/512.00MB native=150.45MB\n2026-04-09 09:02:05.091 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.6  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/20.7  mem=heap=84.63/512.00MB native=150.47MB\n2026-04-09 09:02:06.091 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.11/0.07/0.27/1.00  active=121  pending=28  cacheQ=0  mem=heap=83.75/512.00MB native=150.41MB\n2026-04-09 09:02:06.107 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/19.4  mem=heap=83.76/512.00MB native=150.33MB\n2026-04-09 09:02:07.103 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.3  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.18/0.25  active=121  pending=35  cacheQ=0  mem=heap=83.03/512.00MB native=150.38MB\n2026-04-09 09:02:07.121 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.2  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/21.8  mem=heap=83.32/512.00MB native=150.31MB\n2026-04-09 09:02:08.108 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.7  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.07/0.23/0.24  active=117  pending=37  cacheQ=0  mem=heap=85.38/512.00MB native=150.84MB\n2026-04-09 09:02:08.125 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.7  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.3/22.5  mem=heap=85.67/512.00MB native=150.85MB\n2026-04-09 09:02:09.108 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.20/0.44  active=119  pending=29  cacheQ=0  mem=heap=84.00/512.00MB native=149.84MB\n2026-04-09 09:02:09.126 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/25.8  mem=heap=84.00/512.00MB native=149.82MB\n2026-04-09 09:02:10.126 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.12/0.07/0.34/0.87  active=111  pending=35  cacheQ=0  mem=heap=82.39/512.00MB native=150.14MB\n2026-04-09 09:02:10.144 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.9  mem=heap=82.39/512.00MB native=150.07MB\n2026-04-09 09:02:11.142 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.10/0.08/0.17/0.31  active=108  pending=48  cacheQ=0  mem=heap=86.35/512.00MB native=150.41MB\n2026-04-09 09:02:11.160 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/19.4  mem=heap=86.64/512.00MB native=150.40MB\n2026-04-09 09:02:12.144 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.10/0.08/0.19/0.22  active=109  pending=48  cacheQ=0  mem=heap=85.58/512.00MB native=150.18MB\n2026-04-09 09:02:12.161 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/21.1  mem=heap=85.87/512.00MB native=150.12MB\n2026-04-09 09:02:13.150 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.11/0.08/0.30/0.50  active=113  pending=38  cacheQ=0  mem=heap=83.67/512.00MB native=150.50MB\n2026-04-09 09:02:13.167 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.6  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/20.1  mem=heap=83.95/512.00MB native=150.51MB\n2026-04-09 09:02:14.151 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.21/0.48  active=110  pending=30  cacheQ=0  mem=heap=83.00/512.00MB native=149.91MB\n2026-04-09 09:02:14.168 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/19.5  mem=heap=83.01/512.00MB native=149.89MB\n2026-04-09 09:02:15.153 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.06/0.23/0.31  active=107  pending=20  cacheQ=0  mem=heap=85.69/512.00MB native=150.11MB\n2026-04-09 09:02:15.172 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.7  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/21.2  mem=heap=85.97/512.00MB native=150.01MB\n2026-04-09 09:02:16.164 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.4  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.06/0.30/0.42  active=103  pending=17  cacheQ=0  mem=heap=83.88/512.00MB native=150.05MB\n2026-04-09 09:02:16.182 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.5  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/26.4  mem=heap=83.89/512.00MB native=150.09MB\n2026-04-09 09:02:17.166 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.07/0.05/0.18/0.22  active=97  pending=10  cacheQ=0  mem=heap=82.00/512.00MB native=150.39MB\n2026-04-09 09:02:17.183 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/25.5  mem=heap=82.00/512.00MB native=150.25MB\n2026-04-09 09:02:18.180 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.2  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.18/0.23  active=97  pending=7  cacheQ=0  mem=heap=84.88/512.00MB native=150.33MB\n2026-04-09 09:02:18.195 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.3  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/22.8  mem=heap=84.88/512.00MB native=150.36MB\n2026-04-09 09:02:19.195 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.21/0.26  active=96  pending=17  cacheQ=0  mem=heap=83.47/512.00MB native=150.53MB\n2026-04-09 09:02:19.213 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/27.2  mem=heap=83.48/512.00MB native=150.45MB\n2026-04-09 09:02:20.202 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.6  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.18/0.54  active=91  pending=10  cacheQ=0  mem=heap=81.97/512.00MB native=149.79MB\n2026-04-09 09:02:20.218 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.7  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/22.6  mem=heap=81.98/512.00MB native=149.77MB\n2026-04-09 09:02:21.206 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.06/0.23/0.47  active=92  pending=15  cacheQ=0  mem=heap=85.44/512.00MB native=150.21MB\n2026-04-09 09:02:21.223 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.7  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/27.1  mem=heap=85.73/512.00MB native=150.23MB\n2026-04-09 09:02:22.219 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.3  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.20/0.36  active=89  pending=18  cacheQ=0  mem=heap=83.28/512.00MB native=149.88MB\n2026-04-09 09:02:22.238 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.2  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/23.2  mem=heap=83.55/512.00MB native=149.85MB\n2026-04-09 09:02:23.222 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.06/0.21/0.28  active=93  pending=22  cacheQ=0  mem=heap=82.18/512.00MB native=150.28MB\n2026-04-09 09:02:23.239 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/24.0  mem=heap=82.46/512.00MB native=150.36MB\n2026-04-09 09:02:24.225 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.11/0.06/0.29/1.24  active=92  pending=17  cacheQ=0  mem=heap=85.25/512.00MB native=150.83MB\n2026-04-09 09:02:24.242 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/25.1  mem=heap=85.25/512.00MB native=150.87MB\n2026-04-09 09:02:25.228 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.26/0.28  active=95  pending=16  cacheQ=0  mem=heap=83.95/512.00MB native=151.31MB\n2026-04-09 09:02:25.245 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.9  mem=heap=83.96/512.00MB native=151.28MB\n2026-04-09 09:02:26.244 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.20/0.53  active=98  pending=20  cacheQ=0  mem=heap=82.50/512.00MB native=151.38MB\n2026-04-09 09:02:26.261 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/20.0  mem=heap=82.51/512.00MB native=151.35MB\n2026-04-09 09:02:27.250 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.07/0.17/0.34  active=109  pending=13  cacheQ=0  mem=heap=85.31/512.00MB native=151.36MB\n2026-04-09 09:02:27.267 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.7  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/19.6  mem=heap=85.64/512.00MB native=151.32MB\n2026-04-09 09:02:28.264 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.2  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.07/0.06/0.16/0.18  active=110  pending=15  cacheQ=0  mem=heap=84.79/512.00MB native=150.71MB\n2026-04-09 09:02:28.282 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.2  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/24.9  mem=heap=84.80/512.00MB native=150.75MB\n2026-04-09 09:02:29.282 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.12/0.05/0.27/2.67  active=105  pending=13  cacheQ=0  mem=heap=85.41/512.00MB native=150.27MB\n2026-04-09 09:02:29.299 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/23.8  mem=heap=85.68/512.00MB native=150.31MB\n2026-04-09 09:02:30.298 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.07/0.06/0.17/0.20  active=108  pending=8  cacheQ=0  mem=heap=84.57/512.00MB native=150.41MB\n2026-04-09 09:02:30.316 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/21.5  mem=heap=84.84/512.00MB native=150.32MB\n2026-04-09 09:02:31.299 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.06/0.19/0.39  active=103  pending=11  cacheQ=0  mem=heap=83.61/512.00MB native=150.05MB\n2026-04-09 09:02:31.333 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/23.9  mem=heap=83.88/512.00MB native=150.05MB\n2026-04-09 09:02:32.300 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.07/0.05/0.15/0.25  active=99  pending=13  cacheQ=0  mem=heap=86.11/512.00MB native=150.10MB\n2026-04-09 09:02:32.348 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/23.6  mem=heap=82.63/512.00MB native=150.09MB\n2026-04-09 09:02:33.312 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.3  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.12/0.05/0.45/1.59  active=99  pending=13  cacheQ=0  mem=heap=85.52/512.00MB native=150.05MB\n2026-04-09 09:02:33.365 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/19.4  mem=heap=85.80/512.00MB native=150.03MB\n2026-04-09 09:02:34.330 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.07/0.06/0.15/0.40  active=99  pending=24  cacheQ=0  mem=heap=84.12/512.00MB native=150.37MB\n2026-04-09 09:02:34.381 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/23.9  mem=heap=84.39/512.00MB native=150.33MB\n2026-04-09 09:02:35.334 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.07/0.05/0.19/0.31  active=102  pending=15  cacheQ=0  mem=heap=83.25/512.00MB native=149.73MB\n2026-04-09 09:02:35.385 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/25.8  mem=heap=83.85/512.00MB native=149.78MB\n2026-04-09 09:02:36.340 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.06/0.05/0.13/0.15  active=98  pending=8  cacheQ=0  mem=heap=85.57/512.00MB native=150.10MB\n2026-04-09 09:02:36.392 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.6  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/20.8  mem=heap=85.74/512.00MB native=150.10MB\n2026-04-09 09:02:37.357 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.07/0.05/0.23/0.31  active=93  pending=10  cacheQ=0  mem=heap=84.84/512.00MB native=149.76MB\n2026-04-09 09:02:37.392 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.9  mem=heap=85.11/512.00MB native=149.80MB\n2026-04-09 09:02:38.372 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.08/0.05/0.22/0.26  active=99  pending=12  cacheQ=0  mem=heap=83.66/512.00MB native=150.28MB\n2026-04-09 09:02:38.407 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.2  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/31.0  mem=heap=83.94/512.00MB native=150.29MB\n2026-04-09 09:02:39.373 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.06/0.05/0.19/0.27  active=94  pending=5  cacheQ=0  mem=heap=85.86/512.00MB native=150.17MB\n2026-04-09 09:02:39.426 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/23.0  mem=heap=85.39/512.00MB native=150.18MB\n2026-04-09 09:02:40.374 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.06/0.04/0.16/0.20  active=88  pending=3  cacheQ=0  mem=heap=84.24/512.00MB native=149.31MB\n2026-04-09 09:02:40.426 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.0  mem=heap=84.78/512.00MB native=149.32MB\n2026-04-09 09:02:41.390 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.08/0.06/0.21/0.57  active=103  pending=38  cacheQ=0  mem=heap=83.95/512.00MB native=149.62MB\n2026-04-09 09:02:41.427 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/25.4  mem=heap=84.27/512.00MB native=149.53MB\n2026-04-09 09:02:42.393 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.06/0.25/0.28  active=111  pending=69  cacheQ=0  mem=heap=83.84/512.00MB native=149.70MB\n2026-04-09 09:02:42.427 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/19.9  mem=heap=84.12/512.00MB native=149.62MB\n2026-04-09 09:02:43.398 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.10/0.07/0.30/0.59  active=122  pending=76  cacheQ=1  mem=heap=82.99/512.00MB native=149.61MB\n2026-04-09 09:02:43.432 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.7  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/28.4  mem=heap=83.33/512.00MB native=149.62MB\n2026-04-09 09:02:44.398 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.07/0.18/0.22  active=129  pending=63  cacheQ=0  mem=heap=83.28/512.00MB native=149.64MB\n2026-04-09 09:02:44.434 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/23.4  mem=heap=83.61/512.00MB native=149.61MB\n2026-04-09 09:02:45.410 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.3  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.16/0.25  active=140  pending=77  cacheQ=0  mem=heap=82.75/512.00MB native=149.75MB\n2026-04-09 09:02:45.447 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.3  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/26.2  mem=heap=83.04/512.00MB native=149.65MB\n2026-04-09 09:02:46.427 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.11/0.09/0.33/0.42  active=153  pending=83  cacheQ=0  mem=heap=82.68/512.00MB native=149.96MB\n2026-04-09 09:02:46.462 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/23.7  mem=heap=82.97/512.00MB native=149.91MB\n2026-04-09 09:02:47.432 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.12/0.08/0.23/1.02  active=164  pending=83  cacheQ=1  mem=heap=85.91/512.00MB native=150.04MB\n2026-04-09 09:02:47.464 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.4  mem=heap=86.21/512.00MB native=150.03MB\n2026-04-09 09:02:48.434 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.09/0.15/0.19  active=169  pending=81  cacheQ=0  mem=heap=85.95/512.00MB native=150.07MB\n2026-04-09 09:02:48.470 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.7  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/20.5  mem=heap=86.30/512.00MB native=150.05MB\n2026-04-09 09:02:49.437 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.11/0.08/0.28/0.35  active=175  pending=68  cacheQ=1  mem=heap=84.55/512.00MB native=150.02MB\n2026-04-09 09:02:49.472 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.5  mem=heap=84.85/512.00MB native=150.04MB\n2026-04-09 09:02:50.439 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.07/0.15/0.18  active=173  pending=53  cacheQ=2  mem=heap=87.69/512.00MB native=150.34MB\n2026-04-09 09:02:50.487 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/20.7  mem=heap=87.14/512.00MB native=150.34MB\n2026-04-09 09:02:51.445 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.7  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.20/0.26  active=165  pending=53  cacheQ=0  mem=heap=85.87/512.00MB native=150.36MB\n2026-04-09 09:02:51.498 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.4  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.4/28.1  mem=heap=86.53/512.00MB native=150.35MB\n2026-04-09 09:02:52.448 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.11/0.09/0.22/0.52  active=169  pending=71  cacheQ=0  mem=heap=85.34/512.00MB native=150.41MB\n2026-04-09 09:02:52.501 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.0  mem=heap=85.71/512.00MB native=150.36MB\n2026-04-09 09:02:53.465 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.11/0.09/0.20/0.48  active=163  pending=77  cacheQ=0  mem=heap=84.21/512.00MB native=150.37MB\n2026-04-09 09:02:53.516 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/20.1  mem=heap=84.79/512.00MB native=150.36MB\n2026-04-09 09:02:54.474 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.5  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.12/0.09/0.46/0.88  active=168  pending=68  cacheQ=2  mem=heap=82.66/512.00MB native=150.44MB\n2026-04-09 09:02:54.526 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.4  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.4/21.0  mem=heap=83.24/512.00MB native=150.47MB\n2026-04-09 09:02:55.485 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.4  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.11/0.09/0.29/0.70  active=166  pending=72  cacheQ=0  mem=heap=86.39/512.00MB native=150.41MB\n2026-04-09 09:02:55.537 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.4  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.4/19.2  mem=heap=86.69/512.00MB native=150.37MB\n2026-04-09 09:02:56.485 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.13/0.08/0.37/0.82  active=169  pending=49  cacheQ=0  mem=heap=86.10/512.00MB native=150.49MB\n2026-04-09 09:02:56.538 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.2  mem=heap=86.67/512.00MB native=150.45MB\n2026-04-09 09:02:57.500 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.17/0.39  active=163  pending=57  cacheQ=0  mem=heap=85.09/512.00MB native=150.29MB\n2026-04-09 09:02:57.552 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.2  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/22.3  mem=heap=85.39/512.00MB native=150.22MB\n2026-04-09 09:02:58.516 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.10/0.08/0.27/0.72  active=158  pending=54  cacheQ=0  mem=heap=83.10/512.00MB native=149.98MB\n2026-04-09 09:02:58.569 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/22.3  mem=heap=83.68/512.00MB native=150.00MB\n2026-04-09 09:02:59.532 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.11/0.08/0.25/0.36  active=156  pending=63  cacheQ=0  mem=heap=85.23/512.00MB native=150.01MB\n2026-04-09 09:02:59.584 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/27.6  mem=heap=85.81/512.00MB native=150.03MB\n2026-04-09 09:03:00.544 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.3  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.17/0.20  active=154  pending=56  cacheQ=0  mem=heap=84.33/512.00MB native=150.00MB\n2026-04-09 09:03:00.597 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.3  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/20.0  mem=heap=84.92/512.00MB native=150.01MB\n2026-04-09 09:03:01.546 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.08/0.08/0.12/0.20  active=157  pending=53  cacheQ=0  mem=heap=86.92/512.00MB native=150.01MB\n2026-04-09 09:03:01.600 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/26.0  mem=heap=83.37/512.00MB native=150.09MB\n2026-04-09 09:03:02.551 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.19/0.29  active=154  pending=62  cacheQ=0  mem=heap=85.91/512.00MB native=150.23MB\n2026-04-09 09:03:02.604 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/22.0  mem=heap=86.21/512.00MB native=150.20MB\n2026-04-09 09:03:03.568 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.11/0.08/0.33/0.46  active=155  pending=70  cacheQ=0  mem=heap=85.61/512.00MB native=149.69MB\n2026-04-09 09:03:03.605 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.4  mem=heap=85.91/512.00MB native=149.67MB\n2026-04-09 09:03:04.572 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.14/0.22  active=149  pending=57  cacheQ=0  mem=heap=83.86/512.00MB native=149.66MB\n2026-04-09 09:03:04.609 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/24.9  mem=heap=84.15/512.00MB native=149.67MB\n2026-04-09 09:03:05.573 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.10/0.07/0.19/0.82  active=148  pending=49  cacheQ=0  mem=heap=82.80/512.00MB native=149.74MB\n2026-04-09 09:03:05.627 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/27.3  mem=heap=83.42/512.00MB native=149.71MB\n2026-04-09 09:03:06.576 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.22/0.25  active=154  pending=57  cacheQ=0  mem=heap=86.33/512.00MB native=149.71MB\n2026-04-09 09:03:06.628 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.9  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/22.4  mem=heap=86.48/512.00MB native=149.73MB\n2026-04-09 09:03:07.578 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.10/0.08/0.19/0.66  active=150  pending=60  cacheQ=0  mem=heap=86.08/512.00MB native=149.75MB\n2026-04-09 09:03:07.628 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/20.0  mem=heap=86.43/512.00MB native=149.68MB\n2026-04-09 09:03:08.592 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.2  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.10/0.08/0.29/0.57  active=150  pending=50  cacheQ=2  mem=heap=85.72/512.00MB native=149.68MB\n2026-04-09 09:03:08.628 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/29.2  mem=heap=86.08/512.00MB native=149.64MB\n2026-04-09 09:03:09.593 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.17/0.31  active=150  pending=71  cacheQ=0  mem=heap=86.16/512.00MB native=149.56MB\n2026-04-09 09:03:09.645 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/20.5  mem=heap=86.46/512.00MB native=149.58MB\n2026-04-09 09:03:10.599 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.16/0.50  active=150  pending=51  cacheQ=1  mem=heap=85.41/512.00MB native=149.57MB\n2026-04-09 09:03:10.652 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.6  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.7/24.5  mem=heap=85.69/512.00MB native=149.60MB\n2026-04-09 09:03:11.604 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.12/0.09/0.35/0.68  active=151  pending=62  cacheQ=0  mem=heap=83.86/512.00MB native=149.70MB\n2026-04-09 09:03:11.655 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/23.4  mem=heap=84.13/512.00MB native=149.67MB\n2026-04-09 09:03:12.626 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.14/0.30  active=151  pending=78  cacheQ=1  mem=heap=86.53/512.00MB native=149.73MB\n2026-04-09 09:03:12.671 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/24.7  mem=heap=82.88/512.00MB native=149.71MB\n2026-04-09 09:03:13.629 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.8  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.09/0.14/0.17  active=154  pending=77  cacheQ=0  mem=heap=86.30/512.00MB native=149.77MB\n2026-04-09 09:03:13.684 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.2  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/22.7  mem=heap=86.85/512.00MB native=149.83MB\n2026-04-09 09:03:14.633 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.8  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.09/0.16/0.20  active=151  pending=82  cacheQ=1  mem=heap=85.45/512.00MB native=149.68MB\n2026-04-09 09:03:14.684 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/20.4  mem=heap=85.79/512.00MB native=149.70MB\n2026-04-09 09:03:15.649 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.1  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.16/0.35  active=145  pending=81  cacheQ=0  mem=heap=83.70/512.00MB native=149.67MB\n2026-04-09 09:03:15.685 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.7  mem=heap=84.00/512.00MB native=149.61MB\n2026-04-09 09:03:16.663 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.2  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.08/0.08/0.15/0.17  active=137  pending=75  cacheQ=0  mem=heap=82.43/512.00MB native=149.68MB\n2026-04-09 09:03:16.698 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.3  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/23.1  mem=heap=82.82/512.00MB native=149.71MB\n2026-04-09 09:03:17.664 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.17/0.43  active=141  pending=65  cacheQ=1  mem=heap=85.51/512.00MB native=149.76MB\n2026-04-09 09:03:17.705 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.8  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/20.5  mem=heap=85.85/512.00MB native=149.73MB\n2026-04-09 09:03:18.681 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.0  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.11/0.08/0.32/0.75  active=139  pending=61  cacheQ=0  mem=heap=85.26/512.00MB native=149.49MB\n2026-04-09 09:03:18.717 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/21.4  mem=heap=84.71/512.00MB native=149.57MB\n2026-04-09 09:03:19.683 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.9  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.12/0.08/0.46/0.76  active=142  pending=78  cacheQ=1  mem=heap=83.00/512.00MB native=149.52MB\n2026-04-09 09:03:19.718 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.0  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/23.9  mem=heap=83.27/512.00MB native=149.44MB\n2026-04-09 09:03:20.698 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=57.2  frames=58  dropped=0  actMs(avg/p50/p95/max)=0.12/0.08/0.27/0.70  active=147  pending=66  cacheQ=0  mem=heap=85.23/512.00MB native=149.53MB\n2026-04-09 09:03:20.733 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=57.1  frames=58  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.5/22.8  mem=heap=85.53/512.00MB native=149.43MB\n2026-04-09 09:03:21.703 28950-29035 DanmakuEngine           dev.aaa1115910.bv2.debug             D  [Action] fps=56.7  frames=57  dropped=0  actMs(avg/p50/p95/max)=0.09/0.08/0.18/0.23  active=153  pending=80  cacheQ=1  mem=heap=85.31/512.00MB native=149.68MB\n2026-04-09 09:03:21.739 28950-28950 DanmakuPlayer           dev.aaa1115910.bv2.debug             D  [Draw] fps=56.7  frames=57  dropped=0  sameSnap=0  maxSameSnapStreak=0  snapAgeMs(avg/max)=17.6/25.3  mem=heap=85.65/512.00MB native=149.70MB\n\n\n\nPS D:\\Code\\bv2> node doc/calc_danmaku_averages.js doc/优化5.txt 40\nDraw:                                                                                                                                                                                                                                                              \n  samples: 20\n  avg fps: 54.33\n  avg frames: 54.70\n  avg dropped: 0.15\n  avg sameSnap: 0.15\n  avg maxSameSnapStreak: 0.15\n  max maxSameSnapStreak: 3\n  records with sameSnap>0: 1\n  avg snapAgeMs(avg): 17.24ms\n  avg snapAgeMs(max): 26.67ms\nAction:\n  samples: 20\n  avg fps: 54.32\n  avg frames: 54.65\n  avg dropped: 0.15\n  avg actMs(avg): 0.11ms\n  avg actMs(p50): 0.05ms\n  avg actMs(p95): 0.37ms\n  avg actMs(max): 0.75ms\nMemory:\n  samples: 40\n  avg heap used: 76.58MB\n  avg heap max: 512.00MB\n  avg native used: 150.52MB\nCacheQ:\n  samples: 20\n  avg cacheQ: 0.20\n  min cacheQ: 0\n  max cacheQ: 2\n\n\nPS D:\\Code\\bv2> node doc/calc_danmaku_averages.js doc/优化5.txt 60\nDraw:\n  samples: 30\n  avg fps: 55.18\n  avg frames: 55.53\n  avg dropped: 0.10\n  avg sameSnap: 0.10\n  avg maxSameSnapStreak: 0.10\n  max maxSameSnapStreak: 3\n  records with sameSnap>0: 1\n  avg snapAgeMs(avg): 17.36ms\n  avg snapAgeMs(max): 25.12ms\nAction:\n  samples: 30\n  avg fps: 55.17\n  avg frames: 55.53\n  avg dropped: 0.10\n  avg actMs(avg): 0.10ms\n  avg actMs(p50): 0.05ms\n  avg actMs(p95): 0.32ms\n  avg actMs(max): 0.64ms\nMemory:\n  samples: 60\n  avg heap used: 78.36MB\n  avg heap max: 512.00MB\n  avg native used: 150.36MB\nCacheQ:\n  samples: 30\n  avg cacheQ: 0.20\n  min cacheQ: 0\n  max cacheQ: 2\n\n\nDraw:\n  samples: 113\n  avg fps: 56.50\n  avg frames: 56.96\n  avg dropped: 0.03\n  avg sameSnap: 0.03\n  avg maxSameSnapStreak: 0.03\n  max maxSameSnapStreak: 3\n  records with sameSnap>0: 1\n  avg snapAgeMs(avg): 17.50ms\n  avg snapAgeMs(max): 23.37ms\nAction:\n  samples: 113\n  avg fps: 56.50\n  avg frames: 56.95\n  avg dropped: 0.03\n  avg actMs(avg): 0.09ms\n  avg actMs(p50): 0.06ms\n  avg actMs(p95): 0.25ms\n  avg actMs(max): 0.52ms\nMemory:\n  samples: 226\n  avg heap used: 82.87MB\n  avg heap max: 512.00MB\n  avg native used: 150.15MB\nCacheQ:\n  samples: 113\n  avg cacheQ: 0.19\n  min cacheQ: 0\n  max cacheQ: 2"
  },
  {
    "path": "gradle/androidx.versions.toml",
    "content": "[versions]\nactivity = \"1.13.0\"\ncompose = \"1.11.0-beta02\"\ncompose-constraintlayout = \"1.1.1\"\ncompose-material-icons = \"1.7.8\"\ncompose-material3 = \"1.5.0-alpha16\"\ncompose-material3-adaptive = \"1.2.0\"\n#noinspection GradleDependency https://issuetracker.google.com/issues/361611808\ncompose-tv = \"1.1.0-beta01\"\ncompose-tv-foundation = \"1.0.0-beta01\"\ncore = \"1.18.0\"\ncore-splashscreen = \"1.2.0\"\ndataStore = \"1.2.1\"\nlifecycle = \"2.10.0\"\nmedia3 = \"1.10.0\"\nnavigation = \"2.9.7\"\nroom = \"2.8.4\"\nwebkit = \"1.15.0\"\n\n[libraries]\n# https://developer.android.com/jetpack/androidx/releases/activity\nactivity-compose = { module = \"androidx.activity:activity-compose\", version.ref = \"activity\" }\n\n# https://developer.android.com/jetpack/compose/layout\ncompose-constraintlayout = { module = \"androidx.constraintlayout:constraintlayout-compose\", version.ref = \"compose-constraintlayout\" }\n\n# https://developer.android.com/jetpack/androidx/releases/tv\ncompose-tv-foundation = { module = \"androidx.tv:tv-foundation\", version.ref = \"compose-tv-foundation\" }\ncompose-tv-material = { module = \"androidx.tv:tv-material\", version.ref = \"compose-tv\" }\n\n# https://developer.android.com/jetpack/androidx/releases/compose-ui\ncompose-ui = { module = \"androidx.compose.ui:ui\", version.ref = \"compose\" }\ncompose-ui-test-junit4 = { module = \"androidx.compose.ui:ui-test-junit4\", version.ref = \"compose\" }\ncompose-ui-test-manifest = { module = \"androidx.compose.ui:ui-test-manifest\", version.ref = \"compose\" }\ncompose-ui-util = { module = \"androidx.compose.ui:ui-util\", version.ref = \"compose\" }\n\n# https://developer.android.com/jetpack/androidx/releases/ui\ncompose-ui-tooling = { module = \"androidx.compose.ui:ui-tooling\", version.ref = \"compose\" }\ncompose-ui-tooling-preview = { module = \"androidx.compose.ui:ui-tooling-preview\", version.ref = \"compose\" }\n\n# https://developer.android.com/jetpack/androidx/releases/compose-material\ncompose-material = { module = \"androidx.compose.material:material\", version.ref = \"compose\" }\ncompose-material-icons = { module = \"androidx.compose.material:material-icons-extended\", version.ref = \"compose-material-icons\" }\n\n# https://developer.android.com/jetpack/androidx/releases/compose-material3\ncompose-material3 = { module = \"androidx.compose.material3:material3\", version.ref = \"compose-material3\" }\ncompose-material3-adaptive-navigation-suit = { module = \"androidx.compose.material3:material3-adaptive-navigation-suite\", version.ref = \"compose-material3\" }\ncompose-material3-window-size = { module = \"androidx.compose.material3:material3-window-size-class\", version.ref = \"compose-material3\" }\n\n# https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive\ncompose-material3-adaptive = { module = \"androidx.compose.material3.adaptive:adaptive\", version.ref = \"compose-material3-adaptive\" }\ncompose-material3-adaptive-layout = { module = \"androidx.compose.material3.adaptive:adaptive-layout\", version.ref = \"compose-material3-adaptive\" }\ncompose-material3-adaptive-navigation = { module = \"androidx.compose.material3.adaptive:adaptive-navigation\", version.ref = \"compose-material3-adaptive\" }\n\n# https://developer.android.com/jetpack/androidx/releases/compose-runtime\ncompose-runtime = { module = \"androidx.compose.runtime:runtime\", version.ref = \"compose\" }\ncompose-livedata = { module = \"androidx.compose.runtime:runtime-livedata\", version.ref = \"compose\" }\n\n# https://developer.android.com/jetpack/androidx/releases/core\ncore-ktx = { module = \"androidx.core:core-ktx\", version.ref = \"core\" }\ncore-splashscreen = { module = \"androidx.core:core-splashscreen\", version.ref = \"core-splashscreen\" }\n\n# https://developer.android.com/topic/libraries/architecture/datastore\ndatastore-typed = { module = \"androidx.datastore:datastore\", version.ref = \"dataStore\" }\ndatastore-preferences = { module = \"androidx.datastore:datastore-preferences\", version.ref = \"dataStore\" }\n\n# https://developer.android.com/jetpack/androidx/releases/lifecycle\nlifecycle-runtime-ktx = { module = \"androidx.lifecycle:lifecycle-runtime-ktx\", version.ref = \"lifecycle\" }\nlifecycle-viewmodel-ktx = { module = \"androidx.lifecycle:lifecycle-viewmodel-ktx\", version.ref = \"lifecycle\" }\nlifecycle-viewmodel-compose = { module = \"androidx.lifecycle:lifecycle-viewmodel-compose\", version.ref = \"lifecycle\" }\n\n# https://developer.android.com/jetpack/androidx/releases/media3\nmedia3-common = { module = \"androidx.media3:media3-common\", version.ref = \"media3\" }\nmedia3-datasource-okhttp = { module = \"androidx.media3:media3-datasource-okhttp\", version.ref = \"media3\" }\nmedia3-decoder = { module = \"androidx.media3:media3-decoder\", version.ref = \"media3\" }\nmedia3-exoplayer = { module = \"androidx.media3:media3-exoplayer\", version.ref = \"media3\" }\nmedia3-exoplayer-dash = { module = \"androidx.media3:media3-exoplayer-dash\", version.ref = \"media3\" }\nmedia3-exoplayer-hls = { module = \"androidx.media3:media3-exoplayer-hls\", version.ref = \"media3\" }\nmedia3-ui = { module = \"androidx.media3:media3-ui\", version.ref = \"media3\" }\n\n# https://developer.android.com/jetpack/androidx/releases/guide/navigation\nnavigation-compose = { module = \"androidx.navigation:navigation-compose\", version.ref = \"navigation\" }\n\n# https://developer.android.com/jetpack/androidx/releases/room\nroom-compiler = { module = \"androidx.room:room-compiler\", version.ref = \"room\" }\nroom-ktx = { module = \"androidx.room:room-ktx\", version.ref = \"room\" }\nroom-runtime = { module = \"androidx.room:room-runtime\", version.ref = \"room\" }\nroom-testing = { module = \"androidx.room:room-ktx\", version.ref = \"room\" }\n\n# https://developer.android.com/jetpack/androidx/releases/webkit\nwebkit = { module = \"androidx.webkit:webkit\", version.ref = \"webkit\" }"
  },
  {
    "path": "gradle/gradle.versions.toml",
    "content": "[versions]\nagp = \"8.13.2\"\nkotlin = \"2.2.21\"\nksp = \"2.2.21-2.0.4\"\nprotobuf = \"0.9.6\"\nversions = \"0.53.0\"\n\n[plugins]\nandroid-application = { id = \"com.android.application\", version.ref = \"agp\" }\nandroid-library = { id = \"com.android.library\", version.ref = \"agp\" }\ncompose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\nkotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\nkotlin-jvm = { id = \"org.jetbrains.kotlin.jvm\", version.ref = \"kotlin\" }\nkotlin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\n\n# https://github.com/google/ksp\ngoogle-ksp = { id = \"com.google.devtools.ksp\", version.ref = \"ksp\" }\n\n# https://github.com/google/protobuf-gradle-plugin\ngoogle-protobuf = { id = \"com.google.protobuf\", version.ref = \"protobuf\" }\n\n# https://github.com/ben-manes/gradle-versions-plugin\nversions = { id = \"com.github.ben-manes.versions\", version.ref = \"versions\" }"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\naccompanist = \"0.36.0\"\nbrotli = \"0.1.2\"\nandroidsvg = \"1.4\"\ncoil = \"2.7.0\"\ngeetest-sensebot = \"4.4.4\"\ngrpc = \"1.78.0\"\ngrpc-kotlin = \"1.5.0\"\njsoup = \"1.22.1\"\nkoin = \"4.1.1\"\nkoin-annotations = \"2.3.1\"\nkoin-compose = \"4.1.1\"\nkotlinx-coroutines = \"1.10.2\"\nkotlinx-serialization = \"1.10.0\"\n#noinspection NewerVersionAvailable 3.2 新的缓存机制导致部分响应无法正常获取\nktor = \"3.1.3\"\nktor-jsoup = \"2.3.0\"\nlogging = \"8.0.01\"\nlottie = \"6.7.1\"\nmaterial = \"1.13.0\"\nmaterial-symbols-compose = \"1.0.1\"\nprotobuf = \"4.34.1\"\nqrcode = \"4.5.0\"\nrememberPreference = \"1.1.1\"\nslf4j-android-mvysny = \"2.0.13\"\nslf4j-simple = \"2.0.17\"\n#vlc = \"3.6.5\"\nzxing = \"3.5.4\"\nuiUtil = \"1.10.6\"\n\n[libraries]\n# https://google.github.io/accompanist/\naccompanist-systemuicontroller = { module = \"com.google.accompanist:accompanist-systemuicontroller\", version.ref = \"accompanist\" }\n\n# https://github.com/BigBadaboom/androidsvg\nandroidSvg = { module = \"com.caverock:androidsvg-aar\", version.ref = \"androidsvg\" }\n\n# https://github.com/google/brotli\nbrotli = { module = \"org.brotli:dec\", version.ref = \"brotli\" }\n\n# https://coil-kt.github.io/coil\ncoil-compose = { module = \"io.coil-kt:coil-compose\", version.ref = \"coil\" }\ncoil-gif = { module = \"io.coil-kt:coil-gif\", version.ref = \"coil\" }\ncoil-svg = { module = \"io.coil-kt:coil-svg\", version.ref = \"coil\" }\n\n# https://docs.geetest.com/sensebot/deploy/client/android\ngeetest-sensebot = { module = \"com.geetest.sensebot:sensebot\", version.ref = \"geetest-sensebot\" }\n\n# https://github.com/grpc/grpc-kotlin\ngrpc-kotlin-stub = { module = \"io.grpc:grpc-kotlin-stub\", version.ref = \"grpc-kotlin\" }\ngrpc-android = { module = \"io.grpc:grpc-android\", version.ref = \"grpc\" }\ngrpc-okhttp = { module = \"io.grpc:grpc-okhttp\", version.ref = \"grpc\" }\ngrpc-protobuf = { module = \"io.grpc:grpc-protobuf\", version.ref = \"grpc\" }\ngrpc-protobuf-lite = { module = \"io.grpc:grpc-protobuf-lite\", version.ref = \"grpc\" }\ngrpc-stub = { module = \"io.grpc:grpc-stub\", version.ref = \"grpc\" }\nprotobuf-kotlin = { module = \"com.google.protobuf:protobuf-kotlin\", version.ref = \"protobuf\" }\nprotobuf-kotlin-lite = { module = \"com.google.protobuf:protobuf-kotlin-lite\", version.ref = \"protobuf\" }\n\n# https://jsoup.org/\njsoup = { module = \"org.jsoup:jsoup\", version.ref = \"jsoup\" }\n\n# https://insert-koin.io\nkoin-core = { module = \"io.insert-koin:koin-core\", version.ref = \"koin\" }\nkoin-android = { module = \"io.insert-koin:koin-android\", version.ref = \"koin\" }\nkoin-annotations = { module = \"io.insert-koin:koin-annotations\", version.ref = \"koin-annotations\" }\nkoin-compose = { module = \"io.insert-koin:koin-androidx-compose\", version.ref = \"koin-compose\" }\nkoin-compose-navigation = { module = \"io.insert-koin:koin-androidx-compose-navigation\", version.ref = \"koin-compose\" }\nkoin-ksp-compiler = { module = \"io.insert-koin:koin-ksp-compiler\", version.ref = \"koin-annotations\" }\n\n# https://kotlinlang.org/docs/jvm-test-using-junit.html\nkotlin-test = { module = \"org.jetbrains.kotlin:kotlin-test\" }\n\n# https://github.com/Kotlin/kotlinx.coroutines\nkotlinx-coroutines = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-core\", version.ref = \"kotlinx-coroutines\" }\n\n# https://github.com/Kotlin/kotlinx.serialization\nkotlinx-serialization = { module = \"org.jetbrains.kotlinx:kotlinx-serialization-json\", version.ref = \"kotlinx-serialization\" }\n\n# https://ktor.io/docs\nktor-client-cio = { module = \"io.ktor:ktor-client-cio\", version.ref = \"ktor\" }\nktor-client-content-negotiation = { module = \"io.ktor:ktor-client-content-negotiation\", version.ref = \"ktor\" }\nktor-client-core = { module = \"io.ktor:ktor-client-core\", version.ref = \"ktor\" }\nktor-client-encoding = { module = \"io.ktor:ktor-client-encoding\", version.ref = \"ktor\" }\nktor-client-okhttp = { module = \"io.ktor:ktor-client-okhttp\", version.ref = \"ktor\" }\nktor-client-serialization-kotlinx = { module = \"io.ktor:ktor-serialization-kotlinx-json\", version.ref = \"ktor\" }\nktor-client-websockets = { module = \"io.ktor:ktor-client-websockets\", version.ref = \"ktor\" }\nktor-server-cio = { module = \"io.ktor:ktor-server-cio\", version.ref = \"ktor\" }\nktor-server-core = { module = \"io.ktor:ktor-server-core\", version.ref = \"ktor\" }\n\n# https://github.com/T-Fowl/ktor-jsoup\nktor-jsoup = { module = \"com.tfowl.ktor:ktor-jsoup\", version.ref = \"ktor-jsoup\" }\n\n# https://github.com/MicroUtils/kotlin-logging\nlogging = { module = \"io.github.oshai:kotlin-logging-jvm\", version.ref = \"logging\" }\n\n# https://airbnb.io/lottie\nlottie = { module = \"com.airbnb.android:lottie-compose\", version.ref = \"lottie\" }\n\n# https://github.com/material-components/material-components-android\nmaterial = { module = \"com.google.android.material:material\", version.ref = \"material\" }\n\n# https://github.com/1552980358/material-symbols-compose\nmaterial-symbols-compose-annotation = { module = \"com.github.1552980358.material-symbols-compose:annotation\", version.ref = \"material-symbols-compose\" }\nmaterial-symbols-compose-ksp = { module = \"com.github.1552980358.material-symbols-compose:ksp\", version.ref = \"material-symbols-compose\" }\n\n# https://github.com/g0dkar/qrcode-kotlin\nqrcode = { module = \"io.github.g0dkar:qrcode-kotlin-android\", version.ref = \"qrcode\" }\n\n# https://github.com/burnoo/compose-remember-preference\nrememberPreference = { module = \"dev.burnoo:compose-remember-preference\", version.ref = \"rememberPreference\" }\n\n# https://gitlab.com/mvysny/slf4j-handroid\nslf4j-android-mvysny = { module = \"com.gitlab.mvysny.slf4j:slf4j-handroid\", version.ref = \"slf4j-android-mvysny\" }\n\n# https://www.slf4j.org\nslf4j-simple = { module = \"org.slf4j:slf4j-simple\", version.ref = \"slf4j-simple\" }\n\n# https://code.videolan.org/videolan/vlc-android\n#vlc-android-all = { module = \"org.videolan.android:libvlc-all\", version.ref = \"vlc\" }\n\n# https://github.com/zxing/zxing\nzxing = { module = \"com.google.zxing:core\", version.ref = \"zxing\" }\nui-util = { group = \"androidx.compose.ui\", name = \"ui-util\", version.ref = \"uiUtil\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Wed Jan 01 20:59:26 CST 2025\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.14.3-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\norg.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Kotlin code style for this project: \"official\" or \"obsolete\":\nkotlin.code.style=official\n# Enables namespacing of each library's R class so that its R class includes only the\n# resources declared in the library itself and none from the library's dependencies,\n# thereby reducing the size of the R class for that library\nandroid.nonTransitiveRClass=true\n# Enable K2\nandroid.lint.useK2Uast=true\n# enable optimized resource shrinking\n#android.r8.optimizedResourceShrinking=true\n# gradle-versions-plugin with gradle 8.4\nsystemProp.javax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl\nsystemProp.javax.xml.transform.TransformerFactory=com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl\nsystemProp.javax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "player/.gitignore",
    "content": "/build"
  },
  {
    "path": "player/build.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nplugins {\n    alias(gradleLibs.plugins.android.library)\n    alias(gradleLibs.plugins.compose.compiler)\n    alias(gradleLibs.plugins.kotlin.android)\n}\n\nandroid {\n    namespace = \"${AppConfiguration.appId}.player\"\n    compileSdk = AppConfiguration.compileSdk\n\n    defaultConfig {\n        minSdk = AppConfiguration.minSdk\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles(\"consumer-rules.pro\")\n\n        buildConfigField(\"String\", \"libVLCVersion\", \"\\\"${AppConfiguration.libVLCVersion}\\\"\")\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"r8Test\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"alpha\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n\n    buildFeatures {\n        buildConfig = true\n        compose = true\n    }\n\n    testOptions {\n        targetSdk = AppConfiguration.targetSdk\n    }\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\ndependencies {\n    api(project(\":player:core\"))\n    api(project(\":player:shared\"))\n    api(project(\":player:mobile\"))\n    api(project(\":player:tv\"))\n}"
  },
  {
    "path": "player/core/.gitignore",
    "content": "/build"
  },
  {
    "path": "player/core/build.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nplugins {\n    alias(gradleLibs.plugins.android.library)\n    alias(gradleLibs.plugins.compose.compiler)\n    alias(gradleLibs.plugins.kotlin.android)\n}\n\nandroid {\n    namespace = \"${AppConfiguration.appId}.player.core\"\n    compileSdk = AppConfiguration.compileSdk\n\n    defaultConfig {\n        minSdk = AppConfiguration.minSdk\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles(\"consumer-rules.pro\")\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"r8Test\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"alpha\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n\n    buildFeatures {\n        compose = true\n    }\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\ndependencies {\n    implementation(androidx.activity.compose)\n    implementation(androidx.compose.material)\n    implementation(androidx.compose.tv.foundation)\n    implementation(androidx.compose.tv.material)\n    implementation(androidx.compose.ui)\n    implementation(androidx.compose.ui.tooling.preview)\n    implementation(androidx.compose.ui.util)\n    implementation(androidx.core.ktx)\n    implementation(androidx.media3.common)\n    implementation(androidx.media3.datasource.okhttp)\n    implementation(androidx.media3.decoder)\n    implementation(androidx.media3.exoplayer)\n    implementation(androidx.media3.exoplayer.hls)\n    implementation(androidx.media3.ui)\n    implementation(libs.logging)\n    implementation(libs.material)\n//    implementation(project(\":akdanmaku\"))\n    implementation(project(\":libs:ffmpegDecoder\"))\n    implementation(project(\":player:shared\"))\n    testImplementation(libs.kotlin.test)\n    androidTestImplementation(androidx.compose.ui.test.junit4)\n    debugImplementation(androidx.compose.ui.test.manifest)\n    debugImplementation(androidx.compose.ui.tooling)\n}\n\ntasks.withType<Test> {\n    useJUnitPlatform()\n}"
  },
  {
    "path": "player/core/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "player/core/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "player/core/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n</manifest>"
  },
  {
    "path": "player/core/src/main/kotlin/dev/aaa1115910/bv/player/AbstractVideoPlayer.kt",
    "content": "package dev.aaa1115910.bv.player\n\nabstract class AbstractVideoPlayer {\n    /** 播放器事件回调 */\n    protected var mPlayerEventListener: VideoPlayerListener? = null\n\n    /** 跳转播放位置后的回调 */\n    var onSeek: ((Long) -> Unit)? = null\n\n    /** 解码器错误回调，返回 true 表示已处理（如降级清晰度），false 表示未处理需走正常错误流程 */\n    var onDecoderError: (() -> Boolean)? = null\n\n    /** 标记是否处于后台/生命周期过渡期，用于抑制 Surface 相关的非致命错误 */\n    @Volatile\n    var isInBackground: Boolean = false\n\n    /** 初始跳转位置（毫秒），用于避免在 onReady 中 seek 导致的状态抖动 */\n    protected var pendingSeekPosition: Long = 0L\n\n    /** 设置初始播放位置（毫秒），需在 prepare() 之前调用 */\n    open fun setInitialSeekPosition(position: Long) {\n        pendingSeekPosition = position\n    }\n\n    /** 清除初始播放位置 */\n    protected fun clearPendingSeekPosition() {\n        pendingSeekPosition = 0L\n    }\n\n    /**\n     * 初始化播放器实例\n     * 视频播放器第一步：创建视频播放器\n     */\n    abstract fun initPlayer()\n\n    /** 设置请求头 */\n    abstract fun setHeader(headers: Map<String, String>)\n\n    /** 设置播放地址 */\n    abstract fun playUrl(videoUrl: String? = null, audioUrl: String? = null)\n\n    /** 准备开始播放 */\n    abstract fun prepare()\n\n    /** 播放 */\n    abstract fun start()\n\n    /** 暂停 */\n    abstract fun pause()\n\n    /** 停止 */\n    abstract fun stop()\n\n    /** 重置播放器 */\n    abstract fun reset()\n\n    /** 是否正在播放 */\n    abstract val isPlaying: Boolean\n\n    /** 跳转播放位置 */\n    abstract fun seekTo(time: Long)\n\n    /** 释放播放器 */\n    abstract fun release()\n\n    /** 当前播放位置 */\n    abstract val currentPosition: Long\n\n    /** 视频总时长 */\n    abstract val duration: Long\n\n    /** 缓冲百分比 */\n    abstract val bufferedPercentage: Int\n\n    /** 设置其他播放配置 */\n    abstract fun setOptions()\n\n    /** 播放速度 */\n    abstract var speed: Float\n\n    /** 当前缓冲的网速 */\n    abstract val tcpSpeed: Long\n\n    /** 调试信息 */\n    abstract val debugInfo: String\n\n    /** 额外调试信息（由外部拼接） */\n    var extraDebugInfo: String = \"\"\n\n    /** 是否为直播模式 */\n    var isLive: Boolean = false\n\n    /** 视频宽度 */\n    abstract val videoWidth: Int\n\n    /** 视频高度 */\n    abstract val videoHeight: Int\n\n    /**\n     * 绑定VideoView，监听播放异常，完成，开始准备，视频size变化，视频信息等操作\n     */\n    fun setPlayerEventListener(playerEventListener: VideoPlayerListener?) {\n        mPlayerEventListener = playerEventListener\n    }\n}"
  },
  {
    "path": "player/core/src/main/kotlin/dev/aaa1115910/bv/player/BvVideoPlayer.kt",
    "content": "package dev.aaa1115910.bv.player\n\nimport android.graphics.Matrix\nimport android.view.SurfaceView\nimport android.view.TextureView\nimport androidx.annotation.OptIn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.media3.common.util.UnstableApi\nimport androidx.media3.ui.AspectRatioFrameLayout\nimport androidx.media3.ui.PlayerView\nimport dev.aaa1115910.bv.player.impl.exo.ExoMediaPlayer\nimport io.github.oshai.kotlinlogging.KotlinLogging.logger\n\n@OptIn(UnstableApi::class)\n@Composable\nfun BvVideoPlayer(\n    modifier: Modifier = Modifier,\n    videoPlayer: AbstractVideoPlayer,\n    playerListener: VideoPlayerListener,\n    rotationDegrees: Float = 0f, // 新增参数，视频旋转角度\n    forceUseTextureView: Boolean = false\n) {\n    val logger = logger(\"BvVideoPlayer\")\n    val context = LocalContext.current\n    val density = LocalDensity.current\n    val screenHeight = with(density) { context.resources.displayMetrics.heightPixels.toFloat() }\n    val screenWidth = with(density) { context.resources.displayMetrics.widthPixels.toFloat() }\n\n\n    // Listener registration separated from player lifecycle\n    // so it updates when videoPlayerConfigData changes via recomposition\n    LaunchedEffect(playerListener) {\n        videoPlayer.setPlayerEventListener(playerListener)\n    }\n\n    DisposableEffect(Unit) {\n        onDispose {\n            videoPlayer.release()\n        }\n    }\n\n    when (videoPlayer) {\n        is ExoMediaPlayer -> {\n            var textureView: TextureView? by remember { mutableStateOf(null) }\n            var surfaceView: SurfaceView? by remember { mutableStateOf(null) }\n            var lastRotationDegrees by remember { mutableFloatStateOf(rotationDegrees) }\n\n            fun clearVideoView() {\n                surfaceView?.let {\n                    videoPlayer.mPlayer?.clearVideoSurfaceView(it)\n                }\n                textureView?.let {\n                    videoPlayer.mPlayer?.clearVideoTextureView(it)\n                }\n            }\n            if (forceUseTextureView || rotationDegrees != 0f) {\n                fun applyTextureTransform(tv: TextureView?, degreesRaw: Float) {\n                    tv ?: return\n                    if (rotationDegrees != lastRotationDegrees) {\n                        val time = videoPlayer.currentPosition\n                        videoPlayer.stop()\n                        tv.postDelayed({\n                            val viewWidth = tv.width.toFloat()\n                            val viewHeight = tv.height.toFloat()\n\n                            val pivotX = viewWidth / 2f\n                            val pivotY = viewHeight / 2f\n                            val matrix = Matrix().apply {\n                                // 旋转\n                                setRotate(rotationDegrees, pivotX, pivotY)\n\n                                if (rotationDegrees == 90f || rotationDegrees == -90f) {\n                                    // 缩放（使内容适配反转后的宽高比）\n                                    val scale = minOf(\n                                        screenHeight / viewWidth,\n                                        screenWidth / viewHeight\n                                    )\n                                    // 以中心缩放，需先将缩放偏移到中心\n                                    postScale(scale, scale, pivotX, pivotY)\n                                }\n                            }\n                            tv.setTransform(matrix)\n\n                            videoPlayer.mPlayer?.setVideoTextureView(tv)\n                            videoPlayer.prepare()\n                            videoPlayer.seekTo(time)\n                            videoPlayer.start()\n\n                            lastRotationDegrees = rotationDegrees\n                        }, 0)\n                    }\n                }\n\n                AndroidView(\n                    modifier = modifier,\n                    factory = { ctx ->\n                        clearVideoView()\n                        TextureView(ctx).also { tv ->\n                            textureView = tv\n                            videoPlayer.mPlayer?.setVideoTextureView(tv)\n                            // 切换到 TextureView 当帧即尝试应用旋转\n                            applyTextureTransform(tv, rotationDegrees)\n\n                            logger.info { \"Current view type is TextureView\" }\n                        }\n                    },\n                    update = { tv ->\n                        applyTextureTransform(tv, rotationDegrees)\n                    }\n                )\n            } else {\n                // SurfaceView 渲染\n                AndroidView(\n                    modifier = modifier,\n                    factory = { ctx ->\n                        lastRotationDegrees = rotationDegrees\n                        clearVideoView()\n                        SurfaceView(ctx).also { sv ->\n                            surfaceView = sv\n                            videoPlayer.mPlayer?.setVideoSurfaceView(sv)\n\n                            logger.info { \"Current view type is SurfaceView\" }\n                        }\n                    }\n                )\n            }\n\n            DisposableEffect(videoPlayer) {\n                onDispose {\n                    clearVideoView()\n                    textureView = null\n                    surfaceView = null\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "player/core/src/main/kotlin/dev/aaa1115910/bv/player/OkHttpUtil.kt",
    "content": "package dev.aaa1115910.bv.player\n\nimport android.content.Context\nimport okhttp3.OkHttpClient\nimport java.security.KeyStore\nimport java.security.cert.CertificateFactory\nimport javax.net.ssl.HostnameVerifier\nimport javax.net.ssl.HttpsURLConnection\nimport javax.net.ssl.SSLContext\nimport javax.net.ssl.TrustManagerFactory\nimport javax.net.ssl.X509TrustManager\n\nobject OkHttpUtil {\n    fun generateCustomSslOkHttpClient(context: Context): OkHttpClient {\n        val certificateFactory = CertificateFactory.getInstance(\"X.509\")\n        val customCaMap = mapOf(\n            \"custom:r5\" to \"GlobalSign ECC Root CA R5.crt\"\n        )\n\n        val keyStoreType = KeyStore.getDefaultType()\n        val systemKeyStore = KeyStore.getInstance(\"AndroidCAStore\").apply {\n            load(null, null)\n        }\n        val customKeyStore = KeyStore.getInstance(keyStoreType).apply {\n            load(null, null)\n\n            systemKeyStore.aliases().toList().forEach {\n                setCertificateEntry(it, systemKeyStore.getCertificate(it))\n            }\n            customCaMap.forEach { (alias, caFilename) ->\n                val certificateInputStream = context.assets.open(caFilename)\n                val certificate = certificateFactory.generateCertificate(certificateInputStream)\n                setCertificateEntry(alias, certificate)\n            }\n        }\n\n        val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm()\n        val trustManagerFactory: TrustManagerFactory =\n            TrustManagerFactory.getInstance(tmfAlgorithm).apply {\n                init(customKeyStore)\n            }\n\n        val sslContext: SSLContext = SSLContext.getInstance(\"TLS\").apply {\n            init(null, trustManagerFactory.trustManagers, null)\n        }\n\n        return OkHttpClient.Builder()\n            .sslSocketFactory(\n                sslContext.socketFactory,\n                trustManagerFactory.trustManagers[0] as X509TrustManager\n            )\n            .hostnameVerifier { hostname, session ->\n                // 允许 bilivideo.com 和 bilivideo.cn 域名证书互通\n                val biliDomains = listOf(\"bilivideo.com\", \"bilivideo.cn\")\n                val isBiliDomain = biliDomains.any { domain ->\n                    hostname == domain || hostname.endsWith(\".$domain\")\n                }\n                if (isBiliDomain) {\n                    true\n                } else {\n                    HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)\n                }\n            }\n            .build()\n    }\n}"
  },
  {
    "path": "player/core/src/main/kotlin/dev/aaa1115910/bv/player/VideoPlayerListener.kt",
    "content": "package dev.aaa1115910.bv.player\n\ninterface VideoPlayerListener {\n    /** 异常 */\n    fun onError(error: Exception)\n\n    /**\n     * 准备\n     */\n    fun onReady()\n\n    /** 播放 */\n    fun onPlay()\n\n    /** 暂停 */\n    fun onPause()\n\n    /** 缓冲中 */\n    fun onBuffering()\n\n    /** 播放结束 */\n    fun onEnd()\n\n    /** 空闲，例如播放前 */\n    fun onIdle()\n\n    /** 后退 */\n    fun onSeekBack(seekBackIncrementMs: Long)\n\n    /** 前进 */\n    fun onSeekForward(seekForwardIncrementMs: Long)\n\n    /** 视频尺寸变化 */\n    fun onVideoSizeChanged(width: Int, height: Int) {}\n\n    /** 视频内容首帧已渲染（画面可见） */\n    fun onRenderedFirstFrame() {}\n\n}"
  },
  {
    "path": "player/core/src/main/kotlin/dev/aaa1115910/bv/player/VideoPlayerOptions.kt",
    "content": "package dev.aaa1115910.bv.player\n\ndata class VideoPlayerOptions(\n    val userAgent: String? = null,\n    val referer: String? = null,\n    val enableFfmpegAudioRenderer: Boolean = false,\n    val enableAsyncQueueing: Boolean = true\n)"
  },
  {
    "path": "player/core/src/main/kotlin/dev/aaa1115910/bv/player/factory/PlayerFactory.kt",
    "content": "package dev.aaa1115910.bv.player.factory\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.AbstractVideoPlayer\nimport dev.aaa1115910.bv.player.VideoPlayerOptions\n\nabstract class PlayerFactory<T : AbstractVideoPlayer> {\n    abstract fun create(context: Context, options: VideoPlayerOptions): T\n}"
  },
  {
    "path": "player/core/src/main/kotlin/dev/aaa1115910/bv/player/impl/exo/ExoMediaPlayer.kt",
    "content": "package dev.aaa1115910.bv.player.impl.exo\n\nimport android.content.Context\nimport android.os.Build\nimport androidx.annotation.OptIn\nimport androidx.media3.common.C\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.PlaybackException\nimport androidx.media3.common.Player\nimport androidx.media3.common.util.UnstableApi\nimport androidx.media3.datasource.okhttp.OkHttpDataSource\nimport androidx.media3.exoplayer.DefaultLoadControl\nimport androidx.media3.exoplayer.DefaultRenderersFactory\nimport androidx.media3.exoplayer.ExoPlayer\nimport androidx.media3.exoplayer.Renderer\nimport androidx.media3.exoplayer.source.MediaSource\nimport androidx.media3.exoplayer.source.MergingMediaSource\nimport androidx.media3.exoplayer.source.ProgressiveMediaSource\nimport androidx.media3.exoplayer.hls.HlsMediaSource\nimport dev.aaa1115910.bv.player.AbstractVideoPlayer\nimport dev.aaa1115910.bv.player.OkHttpUtil\nimport dev.aaa1115910.bv.player.VideoPlayerOptions\nimport dev.aaa1115910.bv.util.formatHourMinSec\n\n/**\n * 智能缓冲配置\n */\nprivate data class BufferConfig(\n    val minBufferMs: Int,\n    val maxBufferMs: Int,\n    val backBufferMs: Int,\n    val targetBufferBytes: Int,\n    val prioritizeTime: Boolean // 是否优先考虑时间阈值\n)\n\n@OptIn(UnstableApi::class)\nclass ExoMediaPlayer(\n    private val context: Context,\n    private val options: VideoPlayerOptions\n) : AbstractVideoPlayer(), Player.Listener {\n    var mPlayer: ExoPlayer? = null\n    protected var mMediaSource: MediaSource? = null\n\n    // 实时渲染帧率计算\n    private var lastRenderedFrames: Int = 0\n    private var lastFrameTimestamp: Long = 0L\n    private var realTimeFps: Float = 0f\n\n    @OptIn(UnstableApi::class)\n    private val dataSourceFactory =\n        OkHttpDataSource.Factory(OkHttpUtil.generateCustomSslOkHttpClient(context)).apply {\n            options.userAgent?.let { setUserAgent(it) }\n            options.referer?.let { setDefaultRequestProperties(mapOf(\"referer\" to it)) }\n        }\n\n    init {\n        initPlayer()\n    }\n\n    @OptIn(UnstableApi::class)\n    override fun initPlayer() {\n        val renderersFactory = DefaultRenderersFactory(context).apply {\n            setExtensionRendererMode(\n                when (options.enableFfmpegAudioRenderer) {\n                    true -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON\n                    false -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF\n                }\n            )\n            // setMediaCodecSelector(MediaCodecSelector.PREFER_SOFTWARE)\n            setEnableDecoderFallback(true)\n            // 为 API 23-30 启用异步缓冲队列（API 31+ 已默认启用）\n            if (options.enableAsyncQueueing && Build.VERSION.SDK_INT >= 23 && Build.VERSION.SDK_INT < 31) {\n                @Suppress(\"UNCHECKED_CAST\")\n                forceEnableMediaCodecAsynchronousQueueing()\n            }\n        }\n\n        // 创建智能缓冲策略，根据设备性能和视频质量动态调整\n        val bufferConfig = calculateSmartBufferConfig()\n        val loadControl = DefaultLoadControl.Builder()\n            // 动态设置缓冲区大小\n            .setBufferDurationsMs(\n                bufferConfig.minBufferMs, // 最小缓冲时间\n                bufferConfig.maxBufferMs, // 最大缓冲时间\n                DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, // 开始播放前的缓冲时间\n                DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS // 重新缓冲后的播放缓冲\n            )\n            // 优先考虑时间阈值还是缓冲大小。true：优先考虑时间阈值\n            .setPrioritizeTimeOverSizeThresholds(bufferConfig.prioritizeTime)\n            // 根据系统内存计算缓冲区大小\n            .setTargetBufferBytes(bufferConfig.targetBufferBytes)\n            .setBackBuffer(bufferConfig.backBufferMs, false) // 动态回退缓冲\n            .build()\n\n        mPlayer = ExoPlayer\n            .Builder(context)\n            .setRenderersFactory(renderersFactory)\n            .setLoadControl(loadControl)\n            .setSeekForwardIncrementMs(1000 * 10)\n            .setSeekBackIncrementMs(1000 * 10)\n            .build()\n\n        initListener()\n    }\n\n    private fun initListener() {\n        mPlayer?.addListener(this)\n    }\n\n    @OptIn(UnstableApi::class)\n    override fun setHeader(headers: Map<String, String>) {\n\n    }\n\n    @OptIn(UnstableApi::class)\n    override fun playUrl(videoUrl: String?, audioUrl: String?) {\n        val videoMediaSource = videoUrl?.let { createMediaSource(it) }\n        val audioMediaSource = audioUrl?.let { createMediaSource(it) }\n\n        val mediaSources = listOfNotNull(videoMediaSource, audioMediaSource)\n        mMediaSource = MergingMediaSource(*mediaSources.toTypedArray())\n    }\n\n    /**\n     * 根据 URL 自动选择合适的 MediaSource\n     * - .m3u8 URL 使用 HlsMediaSource（支持 HLS 直播/点播）\n     * - 其他 URL 使用 ProgressiveMediaSource（支持 FLV/MP4 等逐行下载）\n     */\n    @OptIn(UnstableApi::class)\n    private fun createMediaSource(url: String): MediaSource {\n        val uri = android.net.Uri.parse(url)\n        val path = uri.path?.lowercase() ?: \"\"\n        val isHls = path.endsWith(\".m3u8\")\n        val mediaItem = if (isHls && isLive) {\n            MediaItem.Builder()\n                .setUri(uri)\n                .setLiveConfiguration(\n                    MediaItem.LiveConfiguration.Builder()\n                        .setTargetOffsetMs(5000)\n                        .setMaxPlaybackSpeed(1.02f)\n                        .build()\n                )\n                .build()\n        } else {\n            MediaItem.fromUri(uri)\n        }\n        return if (isHls) {\n            HlsMediaSource.Factory(dataSourceFactory)\n                .createMediaSource(mediaItem)\n        } else {\n            ProgressiveMediaSource.Factory(dataSourceFactory)\n                .createMediaSource(mediaItem)\n        }\n    }\n\n    @OptIn(UnstableApi::class)\n    override fun prepare() {\n        mPlayer?.setMediaSource(mMediaSource!!)\n        mPlayer?.prepare()\n        // 处理初始跳转位置，避免在 onReady 中 seek 导致的状态抖动\n        if (pendingSeekPosition > 0) {\n            mPlayer?.seekTo(pendingSeekPosition)\n            clearPendingSeekPosition()\n        }\n    }\n\n    override fun start() {\n        if (isInBackground) {\n            isInBackground = false\n            recoverIfNeeded()\n        }\n        mPlayer?.play()\n    }\n\n    override fun pause() {\n        mPlayer?.pause()\n    }\n\n    override fun stop() {\n        mPlayer?.stop()\n    }\n\n    override fun reset() {\n        TODO(\"Not yet implemented\")\n    }\n\n    override val isPlaying: Boolean\n        get() = mPlayer?.isPlaying == true\n\n    override fun seekTo(time: Long) {\n        mPlayer?.seekTo(time)\n        onSeek?.invoke(time)\n    }\n\n    override fun release() {\n        try {\n            mPlayer?.release()\n            mMediaSource = null\n            mPlayer = null\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n    }\n\n    override val currentPosition: Long\n        get() = mPlayer?.currentPosition ?: 0\n    override val duration: Long\n        get() = mPlayer?.duration ?: 0\n    override val bufferedPercentage: Int\n        get() = mPlayer?.bufferedPercentage ?: 0\n\n    override fun setOptions() {\n        mPlayer?.playWhenReady = true\n    }\n\n    override var speed: Float\n        get() = mPlayer?.playbackParameters?.speed ?: 1f\n        set(value) {\n            mPlayer?.setPlaybackSpeed(value)\n        }\n    override val tcpSpeed: Long\n        get() = 0L\n\n    override fun onPlaybackStateChanged(playbackState: Int) {\n        when (playbackState) {\n            Player.STATE_IDLE -> mPlayerEventListener?.onIdle()\n            Player.STATE_BUFFERING -> mPlayerEventListener?.onBuffering()\n            Player.STATE_READY -> mPlayerEventListener?.onReady()\n            Player.STATE_ENDED -> mPlayerEventListener?.onEnd()\n        }\n    }\n\n    override fun onIsPlayingChanged(isPlaying: Boolean) {\n        if (isPlaying) {\n            mPlayerEventListener?.onPlay()\n        } else {\n            mPlayerEventListener?.onPause()\n        }\n    }\n\n    override fun onSeekBackIncrementChanged(seekBackIncrementMs: Long) {\n        mPlayerEventListener?.onSeekBack(seekBackIncrementMs)\n    }\n\n    override fun onSeekForwardIncrementChanged(seekForwardIncrementMs: Long) {\n        mPlayerEventListener?.onSeekForward(seekForwardIncrementMs)\n    }\n\n    override val debugInfo: String\n        get() {\n            val player = mPlayer ?: return \"player: null\"\n            val playbackState = when (player.playbackState) {\n                Player.STATE_IDLE -> \"IDLE\"\n                Player.STATE_BUFFERING -> \"BUFFERING\"\n                Player.STATE_READY -> \"READY\"\n                Player.STATE_ENDED -> \"ENDED\"\n                else -> \"UNKNOWN\"\n            }\n            val videoDecoderCounters = getVideoDecoderCounters()\n            val droppedFrames = videoDecoderCounters?.droppedBufferCount ?: 0\n            val renderedFrames = videoDecoderCounters?.renderedOutputBufferCount ?: 0\n            updateRealTimeFps(renderedFrames)\n            val fps = realTimeFps\n            val videoBitrate = player.videoFormat?.bitrate ?: 0\n            val audioBitrate = player.audioFormat?.bitrate ?: 0\n            val bufferedMs = player.totalBufferedDuration\n            val base = \"\"\"\n                player: ${androidx.media3.common.MediaLibraryInfo.VERSION_SLASHY}\n                state: $playbackState | speed: ${player.playbackParameters.speed}x\n                time: ${currentPosition.formatHourMinSec()} / ${duration.formatHourMinSec()}\n                buffer: ${bufferedMs / 1000}s ($bufferedPercentage%)\n                resolution: ${player.videoSize.width} x ${player.videoSize.height} @ ${String.format(\"%.1f\", fps)}fps\n                video: ${player.videoFormat?.sampleMimeType ?: \"null\"} (${videoBitrate / 1000}kbps) [${getVideoDecoderName()}]\n                audio: ${player.audioFormat?.sampleMimeType ?: \"null\"} (${audioBitrate / 1000}kbps) [${getAudioRendererName()}]\n                frames: rendered=$renderedFrames dropped=$droppedFrames\n            \"\"\".trimIndent()\n            return if (extraDebugInfo.isNotEmpty()) \"$base\\n$extraDebugInfo\" else base\n        }\n\n    private fun updateRealTimeFps(currentRenderedFrames: Int) {\n        val now = System.nanoTime()\n        val elapsed = (now - lastFrameTimestamp) / 1_000_000_000.0\n        if (lastFrameTimestamp != 0L && elapsed >= 0.5) {\n            val deltaFrames = currentRenderedFrames - lastRenderedFrames\n            realTimeFps = (deltaFrames / elapsed).toFloat()\n            lastRenderedFrames = currentRenderedFrames\n            lastFrameTimestamp = now\n        } else if (lastFrameTimestamp == 0L || elapsed >= 0.5) {\n            lastRenderedFrames = currentRenderedFrames\n            lastFrameTimestamp = now\n        }\n    }\n\n    private fun getVideoDecoderName(): String {\n        val rendererCount = mPlayer?.rendererCount ?: return \"UnknownRenderer\"\n        for (i in 0 until rendererCount) {\n            val renderer = mPlayer!!.getRenderer(i)\n            if (renderer.trackType == C.TRACK_TYPE_VIDEO && renderer.state == Renderer.STATE_STARTED) {\n                return renderer.name\n            }\n        }\n        return \"UnknownRenderer\"\n    }\n\n    @OptIn(UnstableApi::class)\n    private fun getVideoDecoderCounters(): androidx.media3.exoplayer.DecoderCounters? {\n        return try {\n            val rendererCount = mPlayer?.rendererCount ?: return null\n            for (i in 0 until rendererCount) {\n                val renderer = mPlayer!!.getRenderer(i)\n                if (renderer.trackType == C.TRACK_TYPE_VIDEO && renderer is androidx.media3.exoplayer.mediacodec.MediaCodecRenderer) {\n                    val field = androidx.media3.exoplayer.mediacodec.MediaCodecRenderer::class.java\n                        .getDeclaredField(\"decoderCounters\")\n                    field.isAccessible = true\n                    return field.get(renderer) as? androidx.media3.exoplayer.DecoderCounters\n                }\n            }\n            null\n        } catch (_: Exception) {\n            null\n        }\n    }\n\n    private fun getAudioRendererName(): String {\n        val rendererCount = mPlayer?.rendererCount ?: return \"UnknownRenderer\"\n        for (i in 0 until rendererCount) {\n            val renderer = mPlayer!!.getRenderer(i)\n            if (renderer.trackType == C.TRACK_TYPE_AUDIO && renderer.state == Renderer.STATE_STARTED) {\n                return renderer.name\n            }\n        }\n        return \"UnknownRenderer\"\n    }\n\n    override val videoWidth: Int\n        get() = mPlayer?.videoSize?.width ?: 0\n    override val videoHeight: Int\n        get() = mPlayer?.videoSize?.height ?: 0\n\n    override fun onVideoSizeChanged(videoSize: androidx.media3.common.VideoSize) {\n        mPlayerEventListener?.onVideoSizeChanged(videoSize.width, videoSize.height)\n    }\n\n    override fun onRenderedFirstFrame() {\n        mPlayerEventListener?.onRenderedFirstFrame()\n    }\n\n    override fun onPlayerError(error: PlaybackException) {\n        if (isInBackground) return\n        // HLS 直播落后于直播窗口时，跳转到直播最新位置而非报错\n        if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {\n            mPlayer?.let { player ->\n                player.seekToDefaultPosition()\n                player.prepare()\n            }\n            return\n        }\n        // 解码器错误：尝试降级清晰度\n        if (error.errorCode in PlaybackException.ERROR_CODE_DECODER_INIT_FAILED..PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED) {\n            if (onDecoderError?.invoke() == true) return\n        }\n        mPlayerEventListener?.onError(error)\n    }\n\n    /**\n     * 从后台恢复时检查播放器状态，若处于错误态则重新 prepare 恢复播放\n     */\n    fun recoverIfNeeded() {\n        val player = mPlayer ?: return\n        if (player.playerError != null) {\n            println(\"recoverIfNeeded: ${player.playerError}\")\n            val pos = player.currentPosition\n            player.prepare()\n            if (pos > 0) player.seekTo(pos)\n            player.play()\n        }\n    }\n\n    /**\n     * 计算智能缓冲配置\n     * 根据设备性能、可用内存和预期视频质量动态调整缓冲策略\n     */\n    private fun calculateSmartBufferConfig(): BufferConfig {\n        val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager\n        val memoryInfo = android.app.ActivityManager.MemoryInfo()\n        activityManager.getMemoryInfo(memoryInfo)\n\n        // 获取当前可用内存（以字节为单位）\n        val availableMemory = memoryInfo.availMem\n        val totalMemory = memoryInfo.totalMem\n        val isLowRam = activityManager.isLowRamDevice\n\n        // 根据设备性能等级调整策略\n        val deviceTier = when {\n            isLowRam || totalMemory < 3L * 1024 * 1024 * 1024 -> DeviceTier.LOW // 低端设备：小于3GB RAM\n            totalMemory < 6L * 1024 * 1024 * 1024 -> DeviceTier.MID // 中端设备：3-6GB RAM\n            else -> DeviceTier.HIGH // 高端设备：6GB+ RAM\n        }\n        return when (deviceTier) {\n            DeviceTier.LOW -> BufferConfig(\n                minBufferMs = 7000,   // 7秒最小缓冲\n                maxBufferMs = 11000,  // 11秒最大缓冲\n                backBufferMs = 0, // 0秒回退缓冲\n                targetBufferBytes = calculateBufferSize(availableMemory, 0.08, 5, 50), // 8%内存，5-50MB\n                prioritizeTime = false // 改为优先大小限制，严格控制内存使用\n            )\n            DeviceTier.MID -> BufferConfig(\n                minBufferMs = 11000,  // 11秒最小缓冲\n                maxBufferMs = 16000,  // 16秒最大缓冲\n                backBufferMs = 0, // 0秒回退缓冲\n                targetBufferBytes = calculateBufferSize(availableMemory, 0.13, 5, 150), // 13%内存，5-150MB\n                prioritizeTime = false\n            )\n            DeviceTier.HIGH -> BufferConfig(\n                minBufferMs = 12000,  // 12秒最小缓冲\n                maxBufferMs = 22000,  // 22秒最大缓冲\n                backBufferMs = 11000, // 11秒回退缓冲\n                targetBufferBytes = calculateBufferSize(availableMemory, 0.18, 10, 300), // 18%内存，10-300MB\n                prioritizeTime = false\n            )\n        }\n    }\n\n    /**\n     * 设备性能等级\n     */\n    private enum class DeviceTier {\n        LOW, MID, HIGH\n    }\n\n    /**\n     * 计算缓冲区大小\n     */\n    private fun calculateBufferSize(\n        availableMemory: Long,\n        memoryRatio: Double,\n        minMB: Int,\n        maxMB: Int\n    ): Int {\n        val calculatedSize = (availableMemory * memoryRatio).toLong()\n        val minSize = minMB * 1024 * 1024L\n        val maxSize = maxMB * 1024 * 1024L\n\n        return when {\n            calculatedSize < minSize -> minSize.toInt()\n            calculatedSize > maxSize -> maxSize.toInt()\n            else -> calculatedSize.toInt()\n        }\n    }\n}\n"
  },
  {
    "path": "player/core/src/main/kotlin/dev/aaa1115910/bv/player/impl/exo/ExoPlayerFactory.kt",
    "content": "package dev.aaa1115910.bv.player.impl.exo\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.VideoPlayerOptions\nimport dev.aaa1115910.bv.player.factory.PlayerFactory\n\nclass ExoPlayerFactory : PlayerFactory<ExoMediaPlayer>() {\n    override fun create(context: Context, options: VideoPlayerOptions): ExoMediaPlayer {\n        return ExoMediaPlayer(context, options)\n    }\n}"
  },
  {
    "path": "player/mobile/.gitignore",
    "content": "/build"
  },
  {
    "path": "player/mobile/build.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nplugins {\n    alias(gradleLibs.plugins.android.library)\n    alias(gradleLibs.plugins.compose.compiler)\n    alias(gradleLibs.plugins.kotlin.android)\n}\n\nandroid {\n    namespace = \"${AppConfiguration.appId}.player.mobile\"\n    compileSdk = AppConfiguration.compileSdk\n\n    defaultConfig {\n        minSdk = AppConfiguration.minSdk\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles(\"consumer-rules.pro\")\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"r8Test\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"alpha\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n\n    buildFeatures {\n        compose = true\n    }\n\n    testOptions {\n        targetSdk = AppConfiguration.targetSdk\n    }\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\ndependencies {\n    implementation(project(\":player:core\"))\n    implementation(project(\":player:shared\"))\n    implementation(androidx.activity.compose)\n    implementation(androidx.compose.constraintlayout)\n    implementation(androidx.compose.material)\n    implementation(androidx.compose.material.icons)\n    implementation(androidx.compose.material3)\n    implementation(androidx.compose.ui)\n    implementation(androidx.compose.ui.tooling.preview)\n    implementation(androidx.compose.ui.util)\n    implementation(androidx.core.ktx)\n    implementation(androidx.media3.common)\n    implementation(androidx.media3.decoder)\n    implementation(androidx.media3.exoplayer)\n    implementation(androidx.media3.exoplayer.dash)\n    implementation(androidx.media3.exoplayer.hls)\n    implementation(androidx.media3.ui)\n    implementation(libs.logging)\n    implementation(libs.material)\n    debugImplementation(androidx.compose.ui.test.manifest)\n    debugImplementation(androidx.compose.ui.tooling)\n}"
  },
  {
    "path": "player/mobile/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "player/mobile/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "player/mobile/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.VIBRATE\" />\n</manifest>"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/BvPlayer.kt",
    "content": "package dev.aaa1115910.bv.player.mobile\n\nimport android.os.CountDownTimer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport dev.aaa1115910.bv.player.danmaku.DanmakuConfig\nimport dev.aaa1115910.bv.player.danmaku.DanmakuView\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskFrame\nimport dev.aaa1115910.bv.player.AbstractVideoPlayer\nimport dev.aaa1115910.bv.player.BvVideoPlayer\nimport dev.aaa1115910.bv.player.VideoPlayerListener\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.DanmakuType\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerClockData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerDanmakuMasksData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerDebugInfoData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerHistoryData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerLoadStateData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerLogsData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerStateData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerVideoInfoData\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoAspectRatio\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.player.entity.VideoListItem\nimport dev.aaa1115910.bv.player.entity.VideoPlayerClockData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerDebugInfoData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerSeekData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerStateData\nimport dev.aaa1115910.bv.player.mobile.controller.BvPlayerController\nimport dev.aaa1115910.bv.util.countDownTimer\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.withContext\nimport java.util.Calendar\n\n@Composable\nfun BvPlayer(\n    modifier: Modifier = Modifier,\n    isFullScreen: Boolean,\n    onEnterFullScreen: () -> Unit,\n    onExitFullScreen: () -> Unit,\n    onBack: () -> Unit,\n    onClearBackToHistoryData: () -> Unit,\n    onChangeResolution: (Resolution, afterChange: suspend () -> Unit) -> Unit,\n    onChangeVideoCodec: (VideoCodec, afterChange: suspend () -> Unit) -> Unit,\n    onChangeAudio: (Audio, afterChange: suspend () -> Unit) -> Unit,\n    onChangeSpeed: (Float) -> Unit,\n    onToggleDanmaku: (Boolean) -> Unit,\n    onEnabledDanmakuTypesChange: (List<DanmakuType>) -> Unit,\n    onDanmakuOpacityChange: (Float) -> Unit,\n    onDanmakuScaleChange: (Float) -> Unit,\n    onDanmakuAreaChange: (Float) -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit,\n    onLoadNextVideo: () -> Unit,\n    onLoadNewVideo: (VideoListItem) -> Unit,\n    videoPlayer: AbstractVideoPlayer,\n    danmakuView: DanmakuView,\n) {\n    val logger = KotlinLogging.logger(\"BvPlayer\")\n\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val currentConfigData by rememberUpdatedState(videoPlayerConfigData)\n    // val videoPlayerDanmakuMaskData = LocalVideoPlayerDanmakuMasksData.current\n    val videoPlayerHistoryData = LocalVideoPlayerHistoryData.current\n    // val videoPlayerLoadStateData = LocalVideoPlayerLoadStateData.current\n    // val videoPlayerLogsData = LocalVideoPlayerLogsData.current\n    // val videoPlayerVideoInfoData = LocalVideoPlayerVideoInfoData.current\n\n    var showLogs by remember { mutableStateOf(false) }\n    var showBackToHistory by remember { mutableStateOf(false) }\n    var isPlaying by rememberSaveable { mutableStateOf(false) }\n    var isError by remember { mutableStateOf(false) }\n    var isBuffering by remember { mutableStateOf(false) }\n    var exception by remember { mutableStateOf<Exception?>(null) }\n\n    var danmakuConfig by remember { mutableStateOf(DanmakuConfig()) }\n\n    var duration by remember { mutableLongStateOf(0L) }\n    var bufferedPercentage by remember { mutableStateOf(0) }\n    // var currentVideoAspectRatio by remember { mutableStateOf(VideoAspectRatio.Default) }\n    var currentPosition by remember { mutableLongStateOf(0L) }\n    //var currentPlaySpeed by remember { mutableFloatStateOf(Prefs.defaultPlaySpeed) }\n    var aspectRatio by remember { mutableFloatStateOf(16f / 9f) }\n    var lastPlayed by remember { mutableLongStateOf(0L) }\n\n    var clock: Triple<Int, Int, Int> by remember { mutableStateOf(Triple(0, 0, 0)) }\n\n    // var hideLogsTimer: CountDownTimer? by remember { mutableStateOf(null) }\n    var clockRefreshTimer: CountDownTimer? by remember { mutableStateOf(null) }\n    var hideBackToHistoryTimer: CountDownTimer? by remember { mutableStateOf(null) }\n\n    // var currentDanmakuMaskFrame: DanmakuMaskFrame? by remember { mutableStateOf(null) }\n\n\n    val updatePosition = {\n        currentPosition = videoPlayer.currentPosition\n        duration = videoPlayer.duration\n        bufferedPercentage = videoPlayer.bufferedPercentage\n    }\n\n    val applyDanmakuConfig: (DanmakuConfig) -> Unit = { newConfig ->\n        danmakuConfig = newConfig\n        danmakuView.setConfig(newConfig)\n    }\n\n    val initDanmakuConfig: () -> Unit = {\n        val danmakuTypes = videoPlayerConfigData.currentDanmakuEnabledList\n        val allowAll = danmakuTypes.contains(DanmakuType.All)\n        val filterLevel = videoPlayerConfigData.currentDanmakuFilterLevel\n        val factor = videoPlayerConfigData.currentDanmakuRollingDurationFactor\n        val durationMultiplier = 2f - factor\n        applyDanmakuConfig(danmakuConfig.copy(\n            enabled = videoPlayerConfigData.showDanmaku,\n            textSizeScale = (videoPlayerConfigData.currentDanmakuScale * 100).toInt(),\n            allowScroll = allowAll || danmakuTypes.contains(DanmakuType.Rolling),\n            allowTop = allowAll || danmakuTypes.contains(DanmakuType.Top),\n            allowBottom = allowAll || danmakuTypes.contains(DanmakuType.Bottom),\n            minLevel = filterLevel,\n            durationMultiplier = durationMultiplier,\n            opacity = currentConfigData.currentDanmakuOpacity,\n            area = currentConfigData.currentDanmakuArea,\n        ))\n    }\n\n    val updateDanmakuConfigTypeFilter: () -> Unit = {\n        val danmakuTypes = videoPlayerConfigData.currentDanmakuEnabledList\n        val allowAll = danmakuTypes.contains(DanmakuType.All)\n        applyDanmakuConfig(danmakuConfig.copy(\n            allowScroll = allowAll || danmakuTypes.contains(DanmakuType.Rolling),\n            allowTop = allowAll || danmakuTypes.contains(DanmakuType.Top),\n            allowBottom = allowAll || danmakuTypes.contains(DanmakuType.Bottom),\n        ))\n    }\n\n    val toggleDanmakuEnabled: (Boolean) -> Unit = { enabled ->\n        applyDanmakuConfig(danmakuConfig.copy(enabled = enabled))\n    }\n\n    val updateDanmakuConfig: () -> Unit = {\n        applyDanmakuConfig(danmakuConfig.copy(\n            textSizeScale = (videoPlayerConfigData.currentDanmakuScale * 100).toInt(),\n        ))\n    }\n\n    val updateVideoAspectRatio: () -> Unit = {\n        val aspectRatioValue = videoPlayer.videoWidth / videoPlayer.videoHeight.toFloat()\n        aspectRatio = if (aspectRatioValue > 0) aspectRatioValue else 16 / 9f\n    }\n\n    val updateBackToHistory: () -> Unit = {\n        // 此处使用 videoPlayerHistoryData.lastPlayed 无法获取到新值\n        //if (videoPlayerHistoryData.lastPlayed > 0 && hideBackToHistoryTimer == null) {\n        if (lastPlayed > 0 && hideBackToHistoryTimer == null) {\n            logger.info { \"show showBackToHistory: ${videoPlayerHistoryData.lastPlayed}\" }\n            showBackToHistory = true\n            hideBackToHistoryTimer = countDownTimer(5000, 1000, \"hideBackToHistoryTimer\") {\n                showBackToHistory = false\n                hideBackToHistoryTimer = null\n                //playerViewModel.lastPlayed = 0\n                onClearBackToHistoryData()\n            }\n        }\n    }\n\n    val videoPlayerListener = object : VideoPlayerListener {\n        override fun onError(error: Exception) {\n            println(\"onError: $error\")\n            isError = true\n            exception = error.cause as Exception?\n        }\n\n        override fun onReady() {\n            logger.info { \"onReady\" }\n            isError = false\n            exception = null\n            initDanmakuConfig()\n\n            updateVideoAspectRatio()\n            videoPlayer.start()\n\n            //reset default play speed\n            logger.info { \"Reset default play speed: ${videoPlayerConfigData.currentVideoSpeed}\" }\n            videoPlayer.speed = videoPlayerConfigData.currentVideoSpeed\n        }\n\n        override fun onPlay() {\n            logger.info { \"onPlay\" }\n            isPlaying = true\n            isBuffering = false\n            danmakuView.play()\n            updateBackToHistory()\n        }\n\n        override fun onPause() {\n            logger.info { \"onPause\" }\n            isPlaying = false\n        }\n\n        override fun onBuffering() {\n            logger.info { \"onBuffering\" }\n            isBuffering = true\n        }\n\n        override fun onEnd() {\n            logger.info { \"onEnd\" }\n            isPlaying = false\n            onLoadNextVideo()\n        }\n\n        override fun onIdle() {\n            logger.info { \"onIdle\" }\n        }\n\n        override fun onSeekBack(seekBackIncrementMs: Long) {\n            danmakuView.notifySeek(currentPosition)\n        }\n\n        override fun onSeekForward(seekForwardIncrementMs: Long) {\n            danmakuView.notifySeek(currentPosition)\n        }\n\n    }\n\n    LaunchedEffect(Unit) {\n        while (true) {\n            updatePosition()\n            delay(200)\n        }\n    }\n\n    // 同步 videoPlayerHistoryData.lastPlayed 到本地变量\n    LaunchedEffect(videoPlayerHistoryData.lastPlayed) {\n        lastPlayed = videoPlayerHistoryData.lastPlayed.toLong()\n    }\n\n    DisposableEffect(Unit) {\n        clockRefreshTimer = countDownTimer(\n            millisInFuture = Long.MAX_VALUE,\n            countDownInterval = 1000,\n            tag = \"clockRefreshTimer\",\n            showLogs = false,\n            onTick = {\n                val calendar = Calendar.getInstance()\n                val hour = calendar.get(Calendar.HOUR_OF_DAY)\n                val minute = calendar.get(Calendar.MINUTE)\n                val second = calendar.get(Calendar.SECOND)\n                clock = Triple(hour, minute, second)\n            }\n        )\n        onDispose {\n            clockRefreshTimer?.cancel()\n        }\n    }\n\n    CompositionLocalProvider(\n        LocalVideoPlayerSeekData provides VideoPlayerSeekData(\n            duration = duration,\n            position = currentPosition,\n            bufferedPercentage = bufferedPercentage\n        ),\n        LocalVideoPlayerClockData provides VideoPlayerClockData(\n            hour = clock.first,\n            minute = clock.second,\n            second = clock.third\n        ),\n        LocalVideoPlayerStateData provides VideoPlayerStateData(\n            isPlaying = isPlaying,\n            isBuffering = isBuffering,\n            isError = isError,\n            exception = exception,\n            showBackToHistory = showBackToHistory\n        ),\n        LocalVideoPlayerDebugInfoData provides VideoPlayerDebugInfoData(\n            debugInfo = videoPlayer.debugInfo\n        ),\n    ) {\n        BvPlayerController(\n            modifier = modifier,\n            isFullScreen = isFullScreen,\n            onEnterFullScreen = onEnterFullScreen,\n            onExitFullScreen = onExitFullScreen,\n            onBack = onBack,\n            onPlay = { videoPlayer.start() },\n            onPause = { videoPlayer.pause() },\n            onSeekToPosition = { position ->\n                danmakuView.notifySeek(position)\n                videoPlayer.seekTo(position)\n            },\n            onChangeResolution = {\n                val currentTime = currentPosition\n                onChangeResolution(it) {\n                    withContext(Dispatchers.Main) {\n                        videoPlayer.seekTo(currentTime)\n                        videoPlayer.start()\n                    }\n                }\n            },\n            onChangeVideoCodec = {\n                val currentTime = currentPosition\n                onChangeVideoCodec(it) {\n                    withContext(Dispatchers.Main) {\n                        videoPlayer.seekTo(currentTime)\n                        videoPlayer.start()\n                    }\n                }\n            },\n            onChangeAudio = {\n                val currentTime = currentPosition\n                onChangeAudio(it) {\n                    withContext(Dispatchers.Main) {\n                        videoPlayer.seekTo(currentTime)\n                        videoPlayer.start()\n                    }\n                }\n            },\n            onChangeSpeed = { speed ->\n                onChangeSpeed(speed)\n                videoPlayer.speed = speed\n            },\n            onToggleDanmaku = { enabled ->\n                toggleDanmakuEnabled(enabled)\n                onToggleDanmaku(enabled)\n            },\n            onEnabledDanmakuTypesChange = { enabledDanmakuTypes ->\n                onEnabledDanmakuTypesChange(enabledDanmakuTypes)\n                updateDanmakuConfigTypeFilter()\n            },\n            onDanmakuOpacityChange = {\n                onDanmakuOpacityChange(it)\n                applyDanmakuConfig(danmakuConfig.copy(opacity = it))\n            },\n            onDanmakuScaleChange = { scale ->\n                onDanmakuScaleChange(scale)\n                applyDanmakuConfig(danmakuConfig.copy(textSizeScale = (scale * 100).toInt()))\n            },\n            onDanmakuAreaChange = {\n                onDanmakuAreaChange(it)\n                applyDanmakuConfig(danmakuConfig.copy(area = it))\n            },\n            onPlayModeChange = onPlayModeChange,\n            onPlayNewVideo = {\n                //if (!Prefs.incognitoMode) sendHeartbeat()\n                onLoadNewVideo(it)\n            }\n        ) {\n            BvVideoPlayer(\n                modifier = Modifier\n                    .aspectRatio(aspectRatio)\n                    .align(Alignment.Center),\n                videoPlayer = videoPlayer, playerListener = videoPlayerListener\n            )\n            androidx.compose.ui.viewinterop.AndroidView(\n                modifier = Modifier\n                    .fillMaxHeight(),\n                factory = { _ ->\n                    danmakuView.apply {\n                        setPositionProvider { videoPlayer.currentPosition.coerceAtLeast(0L) }\n                        setIsPlayingProvider { videoPlayer.isPlaying }\n                        setPlaybackSpeedProvider { currentConfigData.currentVideoSpeed }\n                        setConfig(danmakuConfig)\n                    }\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/MaterialDarkTheme.kt",
    "content": "package dev.aaa1115910.bv.player.mobile\n\nimport android.os.Build\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalContext\n\n@Composable\nfun MaterialDarkTheme(content: @Composable () -> Unit) {\n    val context = LocalContext.current\n    val colorScheme =\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) dynamicDarkColorScheme(context) else darkColorScheme()\n\n    MaterialTheme(colorScheme = colorScheme) {\n        content()\n    }\n}"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/Media3VideoPlayer.kt",
    "content": "package dev.aaa1115910.bv.player.mobile\n\nimport androidx.annotation.OptIn\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.util.UnstableApi\nimport androidx.media3.datasource.DefaultHttpDataSource\nimport androidx.media3.exoplayer.ExoPlayer\nimport androidx.media3.exoplayer.source.MergingMediaSource\nimport androidx.media3.exoplayer.source.ProgressiveMediaSource\nimport androidx.media3.ui.PlayerView\n\n@OptIn(UnstableApi::class)\n@Composable\nfun Media3VideoPlayer(\n    modifier: Modifier = Modifier,\n    videoPlayer: ExoPlayer\n) {\n    var videoPlayerView: PlayerView? by remember { mutableStateOf(null) }\n    AndroidView(\n        modifier = modifier.fillMaxSize(),\n        factory = { ctx ->\n            videoPlayerView = PlayerView(ctx).apply {\n                player = videoPlayer\n                player?.playWhenReady = true\n                //resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL\n                useController = false\n            }\n            videoPlayerView!!\n        }\n    )\n}\n\n@OptIn(UnstableApi::class)\nfun ExoPlayer.playUrl(videoUrl: String?, audioUrl: String?) {\n    val videoMediaSource = videoUrl?.let {\n        ProgressiveMediaSource.Factory(Media3VideoPlayerConfig.dataSourceFactory)\n            .createMediaSource(MediaItem.fromUri(it))\n    }\n    val audioMediaSource = audioUrl?.let {\n        ProgressiveMediaSource.Factory(Media3VideoPlayerConfig.dataSourceFactory)\n            .createMediaSource(MediaItem.fromUri(it))\n    }\n\n    val mediaSources = listOfNotNull(videoMediaSource, audioMediaSource)\n    setMediaSource(MergingMediaSource(*mediaSources.toTypedArray()))\n}\n\n@UnstableApi\nobject Media3VideoPlayerConfig {\n    private const val userAgent =\n    dev.aaa1115910.biliapi.BiliApiConstants.USER_AGENT_WEB\n    private val header = mapOf(\"referer\" to \"https://www.bilibili.com/\")\n\n    val dataSourceFactory = DefaultHttpDataSource.Factory().apply {\n        setUserAgent(userAgent)\n        setDefaultRequestProperties(header)\n    }\n}"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/NoRippleClickable.kt",
    "content": "package dev.aaa1115910.bv.player.mobile\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\n\nfun Modifier.noRippleClickable(\n    enabled: Boolean = true,\n    onClick: () -> Unit\n): Modifier = composed {\n    clickable(\n        indication = null,\n        interactionSource = remember { MutableInteractionSource() },\n        enabled = enabled\n    ) {\n        onClick()\n    }\n}"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/SeekBar.kt",
    "content": "package dev.aaa1115910.bv.player.mobile\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.gestures.draggable\nimport androidx.compose.foundation.gestures.rememberDraggableState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeContentPadding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SliderColors\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalView\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.seekbar.SeekBarThumb\nimport dev.aaa1115910.bv.player.seekbar.SeekMoveState\nimport dev.aaa1115910.bv.player.seekbar.WavySeekBar\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport kotlinx.coroutines.delay\nimport kotlin.math.max\n\n@Composable\nfun VideoSeekBar(\n    modifier: Modifier = Modifier,\n    duration: Long,\n    position: Long,\n    bufferedPercentage: Int,\n    playing: Boolean,\n    colors: SliderColors = SliderDefaults.colors(),\n    thumb: (@Composable (Modifier, SeekMoveState?) -> Unit)? = null,\n    onPositionChange: ((position: Long, pressing: Boolean) -> Unit)? = null\n) {\n    val density = LocalDensity.current\n    var sliderWidth by remember { mutableStateOf(0.dp) }\n    var thumbOffsetX by remember { mutableFloatStateOf(0f) }\n    var pressing by remember { mutableStateOf(false) }\n    var previewPosition by remember { mutableLongStateOf(0L) }\n    var thumbSize by remember { mutableIntStateOf(0) }\n    var thumbOffsetXMax by remember { mutableFloatStateOf(1f) }\n    val thumbOffsetXMin by remember { mutableFloatStateOf(0f) }\n    var seekMoveState by remember { mutableStateOf<SeekMoveState?>(null) }\n\n    val draggableState = rememberDraggableState {\n        pressing = true\n        thumbOffsetX = (thumbOffsetX + it).coerceIn(thumbOffsetXMin, thumbOffsetXMax)\n        val percent = thumbOffsetX / thumbOffsetXMax\n        previewPosition = (percent * duration).toLong()\n        onPositionChange?.invoke(previewPosition, true)\n        if (it > 0) {\n            seekMoveState = SeekMoveState.Forward\n        } else if (it < 0) {\n            seekMoveState = SeekMoveState.Backward\n        }\n    }\n\n    LaunchedEffect(sliderWidth) {\n        thumbOffsetXMax = with(density) { max(1f, (sliderWidth - 32.dp).toPx()) }\n    }\n\n    LaunchedEffect(position) {\n        if (pressing) return@LaunchedEffect\n        val percent = (position.toFloat() / duration.toFloat()).coerceIn(0f, 1f)\n        thumbOffsetX = (percent * thumbOffsetXMax).coerceIn(thumbOffsetXMin, thumbOffsetXMax)\n    }\n\n    BoxWithConstraints(\n        modifier = modifier\n            .draggable(\n                state = draggableState,\n                orientation = Orientation.Horizontal,\n                onDragStopped = {\n                    seekMoveState = null\n                    onPositionChange?.invoke(previewPosition, false)\n                    delay(200)\n                    pressing = false\n                }\n            )\n            .pointerInput(Unit) {\n                detectTapGestures {\n                    val xDp = with(density) { it.x.toDp() }\n                    val percent =\n                        (xDp.coerceIn(16.dp, sliderWidth - 16.dp) - 16.dp) / (sliderWidth - 32.dp)\n                    val newPosition = (percent * duration).toLong()\n                    onPositionChange?.invoke(newPosition, false)\n                }\n            },\n        contentAlignment = Alignment.Center\n    ) {\n        sliderWidth = this.maxWidth\n        WavySeekBar(\n            modifier = Modifier.padding(horizontal = 16.dp),\n            duration = duration,\n            position = if (pressing) previewPosition else position,\n            bufferedPercentage = bufferedPercentage,\n            colors = colors\n        )\n        Box(modifier = Modifier.fillMaxWidth()) {\n            val thumbModifier = Modifier\n                .onSizeChanged { thumbSize = it.width }\n                .offset { IntOffset(thumbOffsetX.toInt(), 0) }\n            thumb?.invoke(thumbModifier, seekMoveState)\n        }\n    }\n}\n\n\n@Preview(device = \"id:tv_1080p\")\n@Preview\n@Composable\nprivate fun DraggableSeekPreview() {\n    val view = LocalView.current\n\n    var duration by remember { mutableLongStateOf(100000L) }\n    var position by remember { mutableLongStateOf(50000L) }\n    var bufferedPercentage by remember { mutableIntStateOf(66) }\n    var playing by remember { mutableStateOf(true) }\n\n    MaterialTheme {\n        Column(\n            modifier = Modifier.safeContentPadding(),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Button(onClick = { playing = !playing }) {\n                Text(if (playing) \"Pause\" else \"Play\")\n            }\n            Text(\"${position.formatHourMinSec()}/${duration.formatHourMinSec()}\")\n            Box(\n                modifier = if (view.isInEditMode) Modifier\n                else Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center\n            ) {\n                VideoSeekBar(\n                    duration = duration,\n                    position = position,\n                    bufferedPercentage = bufferedPercentage,\n                    thumb = { modifier, seekMoveState ->\n                        if (!view.isInEditMode) {\n                            SeekBarThumb(\n                                modifier = modifier.alpha(0.3f),\n                                state = seekMoveState ?: SeekMoveState.Idle,\n                                idleJsonUrl = \"https://i0.hdslb.com/bfs/garb/item/df917f079cd8175cc851cd1e19a197d810a1c6b7.json\",\n                                movingJsonUrl = \"https://i0.hdslb.com/bfs/garb/item/b61bb387a4c895ef165798102ef322c631a9e4e1.json\",\n                                size = 32.dp\n                            )\n                        } else {\n                            Box(\n                                modifier = modifier\n                                    .size(32.dp)\n                                    .clip(RoundedCornerShape(999.dp))\n                                    .alpha(0.4f)\n                                    .background(Color.Blue)\n                            ) { }\n                        }\n                    },\n                    playing = playing,\n                    onPositionChange = { newPosition, pressing ->\n                        if (!pressing) position = newPosition\n                    }\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/controller/BvPlayerController.kt",
    "content": "package dev.aaa1115910.bv.player.mobile.controller\n\nimport android.app.Activity\nimport android.content.Context\nimport android.media.AudioManager\nimport android.os.Build\nimport android.os.VibrationEffect\nimport android.os.Vibrator\nimport android.provider.Settings\nimport android.util.Log\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.detectDragGestures\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CornerSize\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalView\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.max\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.DanmakuType\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerStateData\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.player.entity.VideoListItem\nimport dev.aaa1115910.bv.player.entity.VideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerSeekData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerStateData\nimport dev.aaa1115910.bv.player.mobile.MaterialDarkTheme\nimport dev.aaa1115910.bv.player.mobile.controller.menu.DanmakuMenu\nimport dev.aaa1115910.bv.player.mobile.controller.menu.DashMenu\nimport dev.aaa1115910.bv.player.mobile.controller.menu.MoreMenu\nimport dev.aaa1115910.bv.player.mobile.controller.menu.SpeedMenu\nimport dev.aaa1115910.bv.player.mobile.controller.menu.VideoListMenu\nimport kotlin.math.absoluteValue\nimport kotlin.math.roundToInt\n\n@Composable\nfun BvPlayerController(\n    modifier: Modifier = Modifier,\n    isFullScreen: Boolean,\n    onEnterFullScreen: () -> Unit,\n    onExitFullScreen: () -> Unit,\n    onBack: () -> Unit,\n    onPlay: () -> Unit,\n    onPause: () -> Unit,\n    onSeekToPosition: (Long) -> Unit,\n    onChangeResolution: (Resolution) -> Unit,\n    onChangeVideoCodec: (VideoCodec) -> Unit,\n    onChangeAudio: (Audio) -> Unit,\n    onChangeSpeed: (Float) -> Unit,\n    onToggleDanmaku: (Boolean) -> Unit,\n    onEnabledDanmakuTypesChange: (List<DanmakuType>) -> Unit,\n    onDanmakuOpacityChange: (Float) -> Unit,\n    onDanmakuScaleChange: (Float) -> Unit,\n    onDanmakuAreaChange: (Float) -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit,\n    onPlayNewVideo: (VideoListItem) -> Unit,\n    content: @Composable BoxScope.() -> Unit\n) {\n    val context = LocalContext.current\n    val density = LocalDensity.current\n    val view = LocalView.current\n\n    //TODO 临时解决方案，应该根据手机垂直方向来屏幕宽度\n    //val screenHeight = with(density) { context.resources.displayMetrics.heightPixels.toDp() }\n    val screenWidth = with(density) { context.resources.displayMetrics.widthPixels.toDp() }\n    val screenHeight = with(density) { context.resources.displayMetrics.heightPixels.toDp() }\n\n    var isMenuOpen by remember { mutableStateOf(false) }\n    val videoContentWidth by animateFloatAsState(\n        targetValue = if (isMenuOpen) 0.7f else 1f\n    )\n    val settingsContentOffset by remember(screenHeight, screenWidth) {\n        derivedStateOf {\n            // 不知为何在预览时获取到的宽度有问题\n            if (view.isInEditMode) max(screenWidth, screenHeight) * ((videoContentWidth) - 0.7f)\n            else screenWidth * (videoContentWidth - 0.7f)\n        }\n    }\n    var menuType by remember { mutableStateOf(MenuType.None) }\n\n    val openMenu: (menu: MenuType) -> Unit = { menu ->\n        println(\"open menu: $menu\")\n        menuType = menu\n        isMenuOpen = true\n    }\n\n    LaunchedEffect(isFullScreen) {\n        if (!isFullScreen) isMenuOpen = false\n    }\n\n    Box(\n        modifier = modifier.fillMaxSize()\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxHeight()\n                .fillMaxWidth(videoContentWidth)\n        ) {\n            BvPlayerControllerVideoContent(\n                modifier = Modifier.fillMaxSize(),\n                isMenuOpen = isMenuOpen,\n                isFullScreen = isFullScreen,\n                onEnterFullScreen = onEnterFullScreen,\n                onExitFullScreen = onExitFullScreen,\n                onBack = onBack,\n                onPlay = onPlay,\n                onPause = onPause,\n                onSeekToPosition = onSeekToPosition,\n                onChangeSpeed = onChangeSpeed,\n                onToggleDanmaku = onToggleDanmaku,\n                onOpenMoreMenu = { openMenu(MenuType.More) },\n                onOpenSpeedMenu = { openMenu(MenuType.Speed) },\n                onOpenResolutionMenu = { openMenu(MenuType.Resolution) },\n                onOpenDanmakuMenu = { openMenu(MenuType.Danmaku) },\n                onOpenListMenu = { openMenu(MenuType.List) },\n                onCloseMenu = { isMenuOpen = false }\n            ) {\n                Box(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .clip(RoundedCornerShape(0.dp))\n                ) {\n                    content()\n                }\n            }\n        }\n\n        Row(\n            modifier = Modifier\n                .align(Alignment.CenterEnd)\n                .fillMaxHeight()\n                .fillMaxWidth(0.3f)\n                .offset {\n                    IntOffset(\n                        x = with(density) { settingsContentOffset.roundToPx() },\n                        y = 0\n                    )\n                }\n                .background(Color.Black)\n        ) {\n            BvPlayerControllerSettings(\n                modifier = Modifier.fillMaxSize(),\n                menuType = menuType,\n                onCloseMenu = { isMenuOpen = false },\n                onChangeResolution = onChangeResolution,\n                onChangeVideoCodec = onChangeVideoCodec,\n                onChangeAudio = onChangeAudio,\n                onChangeSpeed = onChangeSpeed,\n                onEnabledDanmakuTypesChange = onEnabledDanmakuTypesChange,\n                onDanmakuOpacityChange = onDanmakuOpacityChange,\n                onDanmakuScaleChange = onDanmakuScaleChange,\n                onDanmakuAreaChange = onDanmakuAreaChange,\n                onPlayModeChange = onPlayModeChange,\n                onPlayNewVideo = onPlayNewVideo\n            )\n        }\n    }\n}\n\nprivate enum class MenuType {\n    None, Speed, Resolution, Danmaku, List, Subtitle, More\n}\n\n@Composable\nprivate fun BvPlayerControllerSettings(\n    modifier: Modifier = Modifier,\n    menuType: MenuType,\n    onCloseMenu: () -> Unit,\n    onChangeResolution: (Resolution) -> Unit,\n    onChangeVideoCodec: (VideoCodec) -> Unit,\n    onChangeAudio: (Audio) -> Unit,\n    onChangeSpeed: (Float) -> Unit,\n    onEnabledDanmakuTypesChange: (List<DanmakuType>) -> Unit,\n    onDanmakuOpacityChange: (Float) -> Unit,\n    onDanmakuScaleChange: (Float) -> Unit,\n    onDanmakuAreaChange: (Float) -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit,\n    onPlayNewVideo: (VideoListItem) -> Unit\n) {\n    MaterialDarkTheme {\n        Box(\n            modifier = modifier\n        ) {\n            when (menuType) {\n                MenuType.None -> {\n\n                }\n\n                MenuType.Speed -> {\n                    SpeedMenu(\n                        onClickSpeed = onChangeSpeed,\n                        onClose = onCloseMenu\n                    )\n                }\n\n                MenuType.Resolution -> {\n                    DashMenu(\n                        onChangeResolution = onChangeResolution,\n                        onChangeVideoCodec = onChangeVideoCodec,\n                        onChangeAudio = onChangeAudio,\n                        onClose = onCloseMenu\n                    )\n                }\n\n                MenuType.Danmaku -> {\n                    DanmakuMenu(\n                        onEnabledDanmakuTypeChange = onEnabledDanmakuTypesChange,\n                        onDanmakuScaleChange = onDanmakuScaleChange,\n                        onDanmakuOpacityChange = onDanmakuOpacityChange,\n                        onDanmakuAreaChange = onDanmakuAreaChange,\n                        onClose = onCloseMenu\n                    )\n                }\n\n                MenuType.List -> {\n                    VideoListMenu(\n                        onClickVideoListItem = onPlayNewVideo,\n                        onClose = onCloseMenu\n                    )\n                }\n\n                MenuType.Subtitle -> {\n\n                }\n\n                MenuType.More -> {\n                    MoreMenu(\n                        onClose = onCloseMenu,\n                        onPlayModeChange = onPlayModeChange\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun BvPlayerControllerVideoContent(\n    modifier: Modifier = Modifier,\n    isMenuOpen: Boolean,\n    isFullScreen: Boolean,\n    onEnterFullScreen: () -> Unit,\n    onExitFullScreen: () -> Unit,\n    onBack: () -> Unit,\n    onPlay: () -> Unit,\n    onPause: () -> Unit,\n    onSeekToPosition: (Long) -> Unit,\n    onChangeSpeed: (Float) -> Unit,\n    onToggleDanmaku: (Boolean) -> Unit,\n    onOpenMoreMenu: () -> Unit,\n    onOpenSpeedMenu: () -> Unit,\n    onOpenResolutionMenu: () -> Unit,\n    onOpenDanmakuMenu: () -> Unit,\n    onOpenListMenu: () -> Unit,\n    onCloseMenu: () -> Unit,\n    content: @Composable BoxScope.() -> Unit\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val videoPlayerSeekData = LocalVideoPlayerSeekData.current\n    val videoPlayerStateData = LocalVideoPlayerStateData.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    var showBaseUi by remember { mutableStateOf(false) }\n    val isMenuOpen by rememberUpdatedState(isMenuOpen)\n\n    //在手势触发的事件中，直接读取 isPlaying currentTime 参数都只会读取到错误的值，原因未知\n    var isPlaying by remember { mutableStateOf(videoPlayerStateData.isPlaying) }\n    LaunchedEffect(videoPlayerStateData.isPlaying) { isPlaying = videoPlayerStateData.isPlaying }\n    var currentTime by remember { mutableStateOf(videoPlayerSeekData.position) }\n    LaunchedEffect(videoPlayerSeekData.position) { currentTime = videoPlayerSeekData.position }\n\n    var is2xPlaying by remember { mutableStateOf(false) }\n    var isMovingSeek by remember { mutableStateOf(false) }\n    var moveStartTime by remember { mutableStateOf(0L) }\n    var moveMs by remember { mutableStateOf(0L) }\n    var moveStartInSafetyArea by remember { mutableStateOf(false) }\n\n    var isMovingBrightness by remember { mutableStateOf(false) }\n    var movedBrightness by remember { mutableStateOf(false) }\n    var moveStartBrightness by remember { mutableStateOf(0f) }\n    var currentBrightnessProgress by remember { mutableStateOf(0f) }\n\n    var isMovingVolume by remember { mutableStateOf(false) }\n    var moveStartVolume by remember { mutableStateOf(0) }\n    var currentVolumeProgress by remember { mutableStateOf(0f) }\n\n    val onTap: () -> Unit = {\n        Log.i(\"BvPlayerController\", \"Screen tap\")\n        if (isMenuOpen) {\n            onCloseMenu()\n            showBaseUi = true\n        } else {\n            if (!is2xPlaying) showBaseUi = !showBaseUi\n        }\n    }\n\n    val onLongPress: () -> Unit = {\n        Log.i(\"BvPlayerController\", \"Screen long press\")\n        is2xPlaying = true\n        onChangeSpeed(2f)\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n            val vibrator = context.getSystemService(Vibrator::class.java)\n            vibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK))\n        }\n    }\n\n    val onLongPressEnd: (speed: Float) -> Unit = { oldSpeed ->\n        Log.i(\"BvPlayerController\", \"Screen long press end\")\n        is2xPlaying = false\n        onChangeSpeed(oldSpeed)\n    }\n\n    val onDoubleTap: () -> Unit = {\n        Log.i(\n            \"BvPlayerController\",\n            \"Screen double tap, isPlaying: $isPlaying\"\n        )\n        if (isPlaying) onPause() else onPlay()\n    }\n\n    val onHorizontalDrag: (Float) -> Unit = { move ->\n        Log.i(\"BvPlayerController\", \"Screen horizontal drag: $move\")\n        isMovingSeek = true\n        moveMs = move.toLong() * 50\n    }\n\n    val onMovingBrightness: (Float) -> Unit = { move ->\n        Log.i(\"BvPlayerController\", \"Left screen vertical drag: $move\")\n        val window = (context as Activity).window\n        val attr = window.attributes\n        if (isMovingBrightness.not()) {\n            // Settings.System.SCREEN_BRIGHTNESS [0, 255]\n            // attr.screenBrightness [0, 1f]\n            val oldBrightness = if (movedBrightness) attr.screenBrightness else {\n                Settings.System.getInt(\n                    context.contentResolver,\n                    Settings.System.SCREEN_BRIGHTNESS\n                ) / 255f\n            }\n            moveStartBrightness = oldBrightness\n            isMovingBrightness = true\n            movedBrightness = true\n        }\n        val newBrightness = (moveStartBrightness - move / 600).coerceIn(0f, 1f)\n        Log.i(\"BvPlayerController\", \"Brightness: $moveStartBrightness -> $newBrightness\")\n        attr.screenBrightness = newBrightness\n        window.attributes = attr\n        //window.attributes.screenBrightness = newBrightness\n        currentBrightnessProgress = newBrightness\n    }\n\n    val onMovingVolume: (Float) -> Unit = { move ->\n        Log.i(\"BvPlayerController\", \"Right screen vertical drag: $move\")\n        val audioManager =\n            (context as Activity).getSystemService(Context.AUDIO_SERVICE) as AudioManager\n        val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)\n        if (isMovingVolume.not()) {\n            moveStartVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)\n            isMovingVolume = true\n        }\n        val step = maxVolume.toFloat() / 600f\n        val newVolume = ((moveStartVolume - move * step).roundToInt()).coerceIn(0, maxVolume)\n        Log.i(\"BvPlayerController\", \"Volume: $moveStartVolume -> $newVolume\")\n        currentVolumeProgress = newVolume / maxVolume.toFloat()\n        audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, 0)\n    }\n\n    LaunchedEffect(isMovingSeek) {\n        if (isMovingSeek) moveStartTime = videoPlayerSeekData.position\n    }\n\n    LaunchedEffect(is2xPlaying) {\n        if (is2xPlaying) showBaseUi = false\n    }\n\n    Box(\n        modifier = modifier\n            .background(Color.Black)\n    ) {\n        content()\n\n        if (videoPlayerStateData.isBuffering && !videoPlayerStateData.isError) {\n            BufferingTip(modifier = Modifier.align(Alignment.Center))\n        }\n\n        SeekMoveTip(\n            show = isMovingSeek,\n            startTime = moveStartTime,\n            move = moveMs,\n            totalTime = videoPlayerSeekData.duration\n        )\n        BrightnessTip(show = isMovingBrightness, progress = currentBrightnessProgress)\n        VolumeTip(show = isMovingVolume, progress = currentVolumeProgress)\n        QuickDoubleSpeedPlaybackTip(show = is2xPlaying)\n\n        Row(\n            modifier = Modifier.fillMaxSize()\n        ) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .detectPlayerGestures(\n                        enableSafetyArea = isFullScreen,\n                        currentSpeed = videoPlayerConfigData.currentVideoSpeed,\n                        onTap = onTap,\n                        onLongPress = onLongPress,\n                        onLongPressEnd = onLongPressEnd,\n                        onDoubleTap = onDoubleTap,\n                        onVolumeDrag = onMovingVolume,\n                        onBrightnessDrag = onMovingBrightness,\n                        onSeekDrag = onHorizontalDrag,\n                        onDragEnd = { volumeMove, brightnessMove, seekMove ->\n                            Log.i(\n                                \"BvPlayerController\",\n                                \"screen drag end: [volume=$volumeMove, brightness=$brightnessMove, seek=$seekMove]\"\n                            )\n                            if (volumeMove != 0f) {\n                                isMovingVolume = false\n                                Log.i(\"BvPlayerController\", \"Stop move volume\")\n                            } else if (brightnessMove != 0f) {\n                                isMovingBrightness = false\n                                Log.i(\"BvPlayerController\", \"Stop move brightness\")\n                            } else {\n                                isMovingSeek = false\n                                if (moveStartInSafetyArea) {\n                                    moveStartInSafetyArea = false\n                                    return@detectPlayerGestures\n                                }\n                                val seekMoveMs = seekMove.toLong() * 50\n                                onSeekToPosition(moveStartTime + seekMoveMs)\n                                Log.i(\"BvPlayerController\", \"Seek move $seekMoveMs\")\n                            }\n                        }\n                    )\n            ) {}\n        }\n\n        if (showBaseUi) {\n            if (isFullScreen) {\n                FullscreenControllers(\n                    onPlay = onPlay,\n                    onPause = onPause,\n                    onExitFullScreen = onExitFullScreen,\n                    onSeekToPosition = onSeekToPosition,\n                    onShowResolutionController = {\n                        showBaseUi = false\n                        onOpenResolutionMenu()\n                    },\n                    onShowSpeedController = {\n                        showBaseUi = false\n                        onOpenSpeedMenu()\n                    },\n                    onToggleDanmaku = onToggleDanmaku,\n                    onShowDanmakuController = {\n                        showBaseUi = false\n                        onOpenDanmakuMenu()\n                    },\n                    onShowVideoListController = {\n                        showBaseUi = false\n                        onOpenListMenu()\n                    },\n                    onOpenMoreMenu = {\n                        showBaseUi = false\n                        onOpenMoreMenu()\n                    }\n                )\n            } else {\n                MiniControllers(\n                    onBack = onBack,\n                    onPlay = onPlay,\n                    onPause = onPause,\n                    onEnterFullScreen = onEnterFullScreen,\n                    onSeekToPosition = onSeekToPosition\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun BvPlayerControllerSettingsContent(\n    modifier: Modifier = Modifier,\n    content: @Composable BoxScope.() -> Unit\n) {\n    val context = LocalContext.current\n    val colorScheme =\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) dynamicDarkColorScheme(context) else darkColorScheme()\n\n    MaterialTheme(colorScheme = colorScheme) {\n        Surface(\n            modifier = modifier,\n            shape = MaterialTheme.shapes.medium.copy(\n                topEnd = CornerSize(0),\n                bottomEnd = CornerSize(0)\n            ),\n        ) {\n            Box(modifier = Modifier.fillMaxSize()) {\n                content()\n            }\n        }\n    }\n}\n\nfun Modifier.detectPlayerGestures(\n    enableSafetyArea: Boolean,\n    currentSpeed: Float,\n    onTap: () -> Unit,\n    onLongPress: () -> Unit,\n    onLongPressEnd: (speed: Float) -> Unit,\n    onDoubleTap: () -> Unit,\n    onVolumeDrag: (move: Float) -> Unit,\n    onBrightnessDrag: (move: Float) -> Unit,\n    onSeekDrag: (move: Float) -> Unit,\n    onDragEnd: (volumeMove: Float, brightnessMove: Float, seekMove: Float) -> Unit,\n): Modifier = composed {\n    val currentSpeedState = rememberUpdatedState(currentSpeed)\n    var oldPlaySpeed by remember { mutableFloatStateOf(1f) }\n    var componentWidth by remember { mutableIntStateOf(0) }\n    var componentHeight by remember { mutableIntStateOf(0) }\n    val horizontalSafetyArea = 0.1f\n    val verticalSafetyArea = 0.2f\n    var determinedDirection by remember { mutableStateOf(false) }\n    var isHorizontal by remember { mutableStateOf(false) }\n    var horizontalPointMove by remember { mutableFloatStateOf(0f) }\n    var verticalPointMove by remember { mutableFloatStateOf(0f) }\n    var inSafetyArea by remember { mutableStateOf(false) }\n    var longPressing by remember { mutableStateOf(false) }\n    var isMovingVolume by remember { mutableStateOf(false) }\n    var isMovingBrightness by remember { mutableStateOf(false) }\n\n    pointerInput(Unit) {\n        detectTapGestures(\n            onTap = {\n                if (longPressing) return@detectTapGestures\n                onTap()\n            },\n            onLongPress = {\n                onLongPress()\n                oldPlaySpeed = currentSpeedState.value\n                longPressing = true\n            },\n            onDoubleTap = {\n                if (longPressing) return@detectTapGestures\n                onDoubleTap()\n            },\n            onPress = {\n                tryAwaitRelease()\n                if (longPressing) onLongPressEnd(oldPlaySpeed)\n                longPressing = false\n            }\n        )\n    }\n        .onSizeChanged { size ->\n            componentWidth = size.width\n            componentHeight = size.height\n        }\n        .pointerInput(enableSafetyArea) {\n            if (longPressing) return@pointerInput\n\n            detectDragGestures(\n                onDragStart = {\n                    println(\"Drag start: $it, safety x range: (${componentWidth * horizontalSafetyArea}, ${componentWidth * (1 - horizontalSafetyArea)}), safety y range: (${componentHeight * verticalSafetyArea}, ${componentHeight * (1 - verticalSafetyArea)})\")\n                    val inHorizontalSafetyArea =\n                        it.x > componentWidth * horizontalSafetyArea * 0.5f && it.x < componentWidth * (1 - horizontalSafetyArea * 0.5f)\n                    val inVerticalSafetyArea =\n                        it.y > componentHeight * verticalSafetyArea * 0.5f && it.y < componentHeight * (1 - verticalSafetyArea * 0.5f)\n                    inSafetyArea =\n                        inHorizontalSafetyArea && inVerticalSafetyArea || !enableSafetyArea\n                    if (!inSafetyArea) return@detectDragGestures\n\n                    if (it.x < componentWidth * 0.5f) {\n                        isMovingBrightness = true\n                    } else if (it.x >= componentWidth * 0.5f) {\n                        isMovingVolume = true\n                    }\n                },\n                onDragEnd = {\n                    if (!inSafetyArea) return@detectDragGestures\n\n                    if (isHorizontal) {\n                        onDragEnd(0f, 0f, horizontalPointMove)\n                    } else {\n                        if (isMovingVolume) {\n                            onDragEnd(verticalPointMove, 0f, 0f)\n                        } else if (isMovingBrightness) {\n                            onDragEnd(0f, verticalPointMove, 0f)\n                        }\n                    }\n\n                    horizontalPointMove = 0f\n                    verticalPointMove = 0f\n                    determinedDirection = false\n                    isMovingVolume = false\n                    isMovingBrightness = false\n                }\n            ) { _, dragAmount ->\n                if (!inSafetyArea) return@detectDragGestures\n                horizontalPointMove += dragAmount.x\n                verticalPointMove += dragAmount.y\n                if (!determinedDirection) {\n                    if (horizontalPointMove.absoluteValue > 20f) {\n                        determinedDirection = true\n                        isHorizontal = true\n                    } else if (verticalPointMove.absoluteValue > 20f) {\n                        determinedDirection = true\n                        isHorizontal = false\n                    }\n                }\n                if (determinedDirection) {\n                    if (isHorizontal) {\n                        onSeekDrag(horizontalPointMove)\n                    } else {\n                        if (isMovingVolume) {\n                            onVolumeDrag(verticalPointMove)\n                        } else if (isMovingBrightness) {\n                            onBrightnessDrag(verticalPointMove)\n                        }\n                    }\n                }\n            }\n        }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun BufferingTip(\n    modifier: Modifier = Modifier\n) {\n    LoadingIndicator(\n        modifier = modifier.size(120.dp)\n    )\n}\n\n@Preview(device = \"spec:width=1920px,height=1080px\")\n@Composable\nprivate fun BvPlayerControllerPreview() {\n    var isFullScreen by remember { mutableStateOf(false) }\n\n    MaterialTheme {\n        CompositionLocalProvider(\n            LocalVideoPlayerSeekData provides VideoPlayerSeekData(\n                duration = 123456,\n                position = 12345,\n                bufferedPercentage = 60\n            ),\n            LocalVideoPlayerStateData provides VideoPlayerStateData(\n                isPlaying = true,\n            ),\n            LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n                currentResolution = Resolution.R1080P,\n                currentDanmakuEnabled = false\n            )\n        ) {\n            BvPlayerController(\n                isFullScreen = isFullScreen,\n                onEnterFullScreen = { isFullScreen = true },\n                onExitFullScreen = { isFullScreen = false },\n                onBack = {},\n                onPlay = {},\n                onPause = {},\n                onSeekToPosition = {},\n                onChangeResolution = {},\n                onChangeVideoCodec = {},\n                onChangeAudio = {},\n                onChangeSpeed = {},\n                onToggleDanmaku = {},\n                onEnabledDanmakuTypesChange = {},\n                onDanmakuOpacityChange = {},\n                onDanmakuAreaChange = {},\n                onDanmakuScaleChange = {},\n                onPlayModeChange = {},\n                onPlayNewVideo = {}\n            ) {\n                Box(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .background(Color.White)\n                ) {}\n            }\n        }\n    }\n}"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/controller/FullscreenControllers.kt",
    "content": "package dev.aaa1115910.bv.player.mobile.controller\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.ArrowBackIosNew\nimport androidx.compose.material.icons.rounded.ClosedCaption\nimport androidx.compose.material.icons.rounded.FullscreenExit\nimport androidx.compose.material.icons.rounded.MoreVert\nimport androidx.compose.material.icons.rounded.Pause\nimport androidx.compose.material.icons.rounded.PlayArrow\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.IconButtonDefaults.iconButtonColors\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerStateData\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerSeekData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerStateData\nimport dev.aaa1115910.bv.player.mobile.VideoSeekBar\nimport dev.aaa1115910.bv.player.mobile.noRippleClickable\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport dev.aaa1115910.symbols.Subtitles\nimport dev.aaa1115910.symbols.SubtitlesGear\nimport dev.aaa1115910.symbols.SubtitlesOff\nimport me.ks.chan.material.symbols.MaterialSymbols\n\n@Composable\nfun FullscreenControllers(\n    modifier: Modifier = Modifier,\n    onPlay: () -> Unit,\n    onPause: () -> Unit,\n    onExitFullScreen: () -> Unit,\n    onSeekToPosition: (Long) -> Unit,\n    onShowResolutionController: () -> Unit,\n    onShowSpeedController: () -> Unit,\n    onToggleDanmaku: (Boolean) -> Unit,\n    onShowDanmakuController: () -> Unit,\n    onShowVideoListController: () -> Unit,\n    onOpenMoreMenu: () -> Unit\n) {\n    val context = LocalContext.current\n    val videoPlayerSeekData = LocalVideoPlayerSeekData.current\n    val videoPlayerStateData = LocalVideoPlayerStateData.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n    ) {\n        TopControllers(\n            modifier = Modifier\n                .align(Alignment.TopCenter)\n                .fillMaxWidth()\n                .noRippleClickable { },\n            onOpenMoreMenu = onOpenMoreMenu,\n            onExitFullScreen = onExitFullScreen\n        )\n        BottomControllers(\n            modifier = Modifier\n                .align(Alignment.BottomCenter)\n                .fillMaxWidth()\n                .noRippleClickable { },\n            isPlaying = videoPlayerStateData.isPlaying,\n            currentTime = videoPlayerSeekData.position,\n            totalTime = videoPlayerSeekData.duration,\n            bufferedSeekPosition = videoPlayerSeekData.bufferedPercentage,\n            currentResolutionName = videoPlayerConfigData.currentResolution.getDisplayName(context),\n            enabledDanmaku = videoPlayerConfigData.currentDanmakuEnabled,\n            showPartButton = videoPlayerConfigData.availableVideoList.size > 1,\n            onPlay = onPlay,\n            onPause = onPause,\n            onExitFullScreen = onExitFullScreen,\n            onSeekToPosition = onSeekToPosition,\n            onShowResolutionController = onShowResolutionController,\n            onShowSpeedController = onShowSpeedController,\n            onToggleDanmaku = onToggleDanmaku,\n            onShowDanmakuController = onShowDanmakuController,\n            onShowVideoListController = onShowVideoListController\n        )\n    }\n}\n\n@Composable\nprivate fun TopControllers(\n    modifier: Modifier = Modifier,\n    onOpenMoreMenu: () -> Unit,\n    onExitFullScreen: () -> Unit,\n) {\n    Box(\n        modifier = modifier\n        //.background(Color.Black.copy(alpha = 0.6f))\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(8.dp)\n                .height(48.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.SpaceBetween\n        ) {\n            ControllerButtonGroup {\n                IconButton(\n                    onClick = onExitFullScreen,\n                    colors = IconButtonDefaults.iconButtonColors(\n                        contentColor = Color.White\n                    )\n                ) {\n                    Icon(imageVector = Icons.Rounded.ArrowBackIosNew, contentDescription = null)\n                }\n                Text(\n                    modifier = Modifier.padding(end = 12.dp),\n                    text = \"这是一个标题\",\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    color = Color.White\n                )\n            }\n            ControllerButtonGroup {\n                IconButton(\n                    onClick = {},\n                    colors = IconButtonDefaults.iconButtonColors(\n                        contentColor = Color.White\n                    )\n                ) {\n                    Icon(imageVector = Icons.Rounded.ClosedCaption, contentDescription = null)\n                }\n                IconButton(\n                    onClick = onOpenMoreMenu,\n                    colors = IconButtonDefaults.iconButtonColors(\n                        contentColor = Color.White\n                    )\n                ) {\n                    Icon(imageVector = Icons.Rounded.MoreVert, contentDescription = null)\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun BottomControllers(\n    modifier: Modifier = Modifier,\n    isPlaying: Boolean,\n    currentTime: Long,\n    totalTime: Long,\n    bufferedSeekPosition: Int,\n    currentResolutionName: String,\n    enabledDanmaku: Boolean,\n    showPartButton: Boolean,\n    onPlay: () -> Unit,\n    onPause: () -> Unit,\n    onExitFullScreen: () -> Unit,\n    onSeekToPosition: (Long) -> Unit,\n    onShowResolutionController: () -> Unit,\n    onShowSpeedController: () -> Unit,\n    onToggleDanmaku: (Boolean) -> Unit,\n    onShowDanmakuController: () -> Unit,\n    onShowVideoListController: () -> Unit\n) {\n    Box(\n        modifier = modifier\n        //.background(Color.Black.copy(alpha = 0.6f))\n    ) {\n        Column {\n            VideoSeekBar(\n                modifier = Modifier.padding(bottom = 8.dp),\n                duration = totalTime,\n                position = currentTime,\n                bufferedPercentage = bufferedSeekPosition,\n                playing = isPlaying,\n                onPositionChange = { newPosition, isPressing ->\n                    if (!isPressing) onSeekToPosition(newPosition)\n                }\n            )\n\n            ProvideTextStyle(TextStyle(color = Color.White)) {\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(8.dp),\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Row(\n                        Modifier.height(48.dp),\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        IconButton(\n                            modifier = Modifier\n                                .width(80.dp)\n                                .height(48.dp),\n                            onClick = if (isPlaying) onPause else onPlay,\n                            shape = if (isPlaying) MaterialTheme.shapes.medium else MaterialTheme.shapes.extraLarge,\n                            colors = if (isPlaying) {\n                                iconButtonColors(\n                                    containerColor = Color.Black.copy(0.6f),\n                                    contentColor = Color.White\n                                )\n                            } else {\n                                iconButtonColors(\n                                    containerColor = Color.White,\n                                    contentColor = Color.Black\n                                )\n                            }\n                        ) {\n                            if (isPlaying) {\n                                Icon(\n                                    imageVector = Icons.Rounded.Pause,\n                                    contentDescription = null,\n                                )\n                            } else {\n                                Icon(\n                                    imageVector = Icons.Rounded.PlayArrow,\n                                    contentDescription = null,\n                                )\n                            }\n                        }\n                        ControllerButtonGroup {\n                            Text(\n                                modifier = Modifier\n                                    .padding(horizontal = 8.dp),\n                                text = \"${currentTime.formatHourMinSec()}/${totalTime.formatHourMinSec()}\",\n                                color = Color.White,\n                                textAlign = TextAlign.Center\n                            )\n                        }\n                        ControllerButtonGroup {\n                            IconButton(\n                                onClick = { onToggleDanmaku(!enabledDanmaku) },\n                                colors = IconButtonDefaults.iconButtonColors(\n                                    contentColor = Color.White\n                                )\n                            ) {\n                                if (enabledDanmaku) {\n                                    Icon(\n                                        imageVector = MaterialSymbols.Subtitles.Rounded,\n                                        contentDescription = null\n                                    )\n                                } else {\n                                    Icon(\n                                        imageVector = MaterialSymbols.SubtitlesOff.Rounded,\n                                        contentDescription = null\n                                    )\n                                }\n                            }\n                            IconButton(\n                                onClick = onShowDanmakuController,\n                                colors = IconButtonDefaults.iconButtonColors(\n                                    contentColor = Color.White\n                                )\n                            ) {\n                                Icon(\n                                    imageVector = MaterialSymbols.SubtitlesGear.Rounded,\n                                    contentDescription = null\n                                )\n                            }\n                        }\n                    }\n\n                    Row(\n                        Modifier.height(48.dp)\n                    ) {\n                        ControllerButtonGroup {\n                            if (showPartButton) {\n                                TextButton(onClick = onShowVideoListController) {\n                                    Text(text = \"选集\")\n                                }\n                            }\n                            TextButton(onClick = onShowSpeedController) {\n                                Text(text = \"倍速\")\n                            }\n                            TextButton(onClick = onShowResolutionController) {\n                                Text(text = currentResolutionName)\n                            }\n                            IconButton(onClick = onExitFullScreen) {\n                                Icon(\n                                    imageVector = Icons.Rounded.FullscreenExit,\n                                    contentDescription = null,\n                                    tint = Color.White\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun ControllerButtonGroup(\n    modifier: Modifier = Modifier,\n    content: @Composable RowScope.() -> Unit\n) {\n    Box(\n        modifier = modifier\n            .fillMaxHeight()\n            .clip(MaterialTheme.shapes.extraLarge)\n            .background(Color.Black.copy(0.6f)),\n        contentAlignment = Alignment.Center\n    ) {\n        Row(\n            modifier = Modifier\n                .padding(horizontal = 8.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            content()\n        }\n    }\n}\n\n@Preview(device = \"spec:width=1920px,height=1080px\")\n@Composable\nfun FullscreenControllerLightBackgroundPreview() {\n    CompositionLocalProvider(\n        LocalVideoPlayerSeekData provides VideoPlayerSeekData(\n            duration = 123456,\n            position = 12345,\n            bufferedPercentage = 60\n        ),\n        LocalVideoPlayerStateData provides VideoPlayerStateData(\n            isPlaying = true,\n        ),\n        LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n            currentResolution = Resolution.R1080P,\n            currentDanmakuEnabled = false\n        )\n    ) {\n        FullscreenControllers(\n            modifier = Modifier.background(lightColorScheme().surfaceContainer),\n            onPlay = {},\n            onPause = {},\n            onExitFullScreen = {},\n            onSeekToPosition = {},\n            onShowResolutionController = {},\n            onShowSpeedController = {},\n            onToggleDanmaku = {},\n            onShowDanmakuController = {},\n            onShowVideoListController = {},\n            onOpenMoreMenu = {}\n        )\n    }\n}\n\n@Preview(device = \"spec:width=1920px,height=1080px\", backgroundColor = 0xFFFFFFFF)\n@Composable\nfun FullscreenControllerDarkBackgroundPreview() {\n    CompositionLocalProvider(\n        LocalVideoPlayerSeekData provides VideoPlayerSeekData(\n            duration = 123456,\n            position = 12345,\n            bufferedPercentage = 60\n        ),\n        LocalVideoPlayerStateData provides VideoPlayerStateData(\n            isPlaying = true,\n        ),\n        LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n            currentResolution = Resolution.R1080P,\n            currentDanmakuEnabled = false\n        )\n    ) {\n        FullscreenControllers(\n            modifier = Modifier.background(darkColorScheme().surfaceContainerHighest),\n            onPlay = {},\n            onPause = {},\n            onExitFullScreen = {},\n            onSeekToPosition = {},\n            onShowResolutionController = {},\n            onShowSpeedController = {},\n            onToggleDanmaku = {},\n            onShowDanmakuController = {},\n            onShowVideoListController = {},\n            onOpenMoreMenu = {}\n        )\n    }\n}\n"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/controller/MiniControllers.kt",
    "content": "package dev.aaa1115910.bv.player.mobile.controller\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.ArrowBack\nimport androidx.compose.material.icons.rounded.Fullscreen\nimport androidx.compose.material.icons.rounded.Pause\nimport androidx.compose.material.icons.rounded.PlayArrow\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.constraintlayout.compose.ConstraintLayout\nimport androidx.constraintlayout.compose.Dimension\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerStateData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerSeekData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerStateData\nimport dev.aaa1115910.bv.player.mobile.VideoSeekBar\nimport dev.aaa1115910.bv.util.formatHourMinSec\n\n@Composable\nfun MiniControllers(\n    modifier: Modifier = Modifier,\n    onBack: () -> Unit,\n    onPlay: () -> Unit,\n    onPause: () -> Unit,\n    onEnterFullScreen: () -> Unit,\n    onSeekToPosition: (Long) -> Unit,\n) {\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n    ) {\n        TopControllers(\n            modifier = Modifier\n                .align(Alignment.TopCenter)\n                .fillMaxWidth(),\n            onBack = onBack\n        )\n        BottomControllers(\n            modifier = Modifier\n                .align(Alignment.BottomCenter)\n                .fillMaxWidth(),\n            onPlay = onPlay,\n            onPause = onPause,\n            onEnterFullScreen = onEnterFullScreen,\n            onSeekToPosition = onSeekToPosition\n        )\n    }\n}\n\n@Composable\nprivate fun TopControllers(\n    modifier: Modifier = Modifier,\n    onBack: () -> Unit\n) {\n    Box(\n        modifier = modifier\n            .background(Color.Black.copy(alpha = 0.6f))\n    ) {\n        Row(\n            modifier = Modifier.padding(horizontal = 8.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            IconButton(onClick = onBack) {\n                Icon(\n                    imageVector = Icons.Rounded.ArrowBack,\n                    contentDescription = null,\n                    tint = Color.White\n                )\n            }\n            Text(\n                text = \"这是一个标题\",\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                color = Color.White\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun BottomControllers(\n    modifier: Modifier = Modifier,\n    onPlay: () -> Unit,\n    onPause: () -> Unit,\n    onEnterFullScreen: () -> Unit,\n    onSeekToPosition: (Long) -> Unit\n) {\n    val videoPlayerSeekData = LocalVideoPlayerSeekData.current\n    val videoPlayerStateData = LocalVideoPlayerStateData.current\n    Box(\n        modifier = modifier\n            .background(Color.Black.copy(alpha = 0.6f))\n    ) {\n        ConstraintLayout(\n            modifier = Modifier.padding(horizontal = 8.dp)\n        ) {\n            val (playButton, seekSlider, positionText, fullscreenButton) = createRefs()\n\n            IconButton(\n                modifier = Modifier\n                    .constrainAs(playButton) {\n                        top.linkTo(parent.top)\n                        start.linkTo(parent.start)\n                        bottom.linkTo(parent.bottom)\n                    },\n                onClick = { if (videoPlayerStateData.isPlaying) onPause() else onPlay() },\n                colors = IconButtonDefaults.iconButtonColors(\n                    contentColor = Color.White\n                )\n            ) {\n                if (videoPlayerStateData.isPlaying) {\n                    Icon(\n                        imageVector = Icons.Rounded.Pause,\n                        contentDescription = null,\n                    )\n                } else {\n                    Icon(\n                        imageVector = Icons.Rounded.PlayArrow,\n                        contentDescription = null,\n                    )\n                }\n            }\n\n            VideoSeekBar(\n                modifier = Modifier.constrainAs(seekSlider) {\n                    top.linkTo(parent.top)\n                    start.linkTo(playButton.end)\n                    bottom.linkTo(parent.bottom)\n                    end.linkTo(positionText.start)\n                    width = Dimension.preferredWrapContent\n                },\n                duration = videoPlayerSeekData.duration,\n                position = videoPlayerSeekData.position,\n                bufferedPercentage = videoPlayerSeekData.bufferedPercentage,\n                playing = videoPlayerStateData.isPlaying,\n                onPositionChange = { newPosition, isPressing ->\n                    if (!isPressing) onSeekToPosition(newPosition)\n                }\n            )\n\n            Text(\n                modifier = Modifier.constrainAs(positionText) {\n                    top.linkTo(parent.top)\n                    bottom.linkTo(parent.bottom)\n                    end.linkTo(fullscreenButton.start)\n                },\n                text = \"${videoPlayerSeekData.position.formatHourMinSec()}/${videoPlayerSeekData.duration.formatHourMinSec()}\",\n                color = Color.White\n            )\n\n            IconButton(\n                modifier = Modifier.constrainAs(fullscreenButton) {\n                    top.linkTo(parent.top)\n                    end.linkTo(parent.end)\n                    bottom.linkTo(parent.bottom)\n                },\n                onClick = onEnterFullScreen\n            ) {\n                Icon(\n                    imageVector = Icons.Rounded.Fullscreen,\n                    contentDescription = null,\n                    tint = Color.White\n                )\n            }\n        }\n    }\n}\n\n@Preview(device = \"spec:width=1080px,height=600px\")\n@Composable\nfun MiniControllerPreview() {\n    MaterialTheme {\n        CompositionLocalProvider(\n            LocalVideoPlayerSeekData provides VideoPlayerSeekData(\n                duration = 123456,\n                position = 12345,\n                bufferedPercentage = 60\n            ),\n            LocalVideoPlayerStateData provides VideoPlayerStateData(\n                isPlaying = true\n            )\n        ) {\n            MiniControllers(\n                onBack = {},\n                onPlay = {},\n                onPause = {},\n                onEnterFullScreen = {},\n                onSeekToPosition = {},\n            )\n        }\n    }\n}"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/controller/Tips.kt",
    "content": "package dev.aaa1115910.bv.player.mobile.controller\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.BrightnessHigh\nimport androidx.compose.material.icons.rounded.BrightnessLow\nimport androidx.compose.material.icons.rounded.BrightnessMedium\nimport androidx.compose.material.icons.rounded.VolumeDown\nimport androidx.compose.material.icons.rounded.VolumeOff\nimport androidx.compose.material.icons.rounded.VolumeUp\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\n@Composable\nfun SeekMoveTip(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    startTime: Long,\n    move: Long,\n    totalTime: Long\n) {\n    if (show) {\n        Box(\n            modifier = modifier\n                .fillMaxSize()\n        ) {\n            Surface(\n                modifier = Modifier\n                    .align(Alignment.Center),\n                color = Color.Black.copy(alpha = 0.4f),\n                shape = MaterialTheme.shapes.medium\n            ) {\n                Text(\n                    modifier = Modifier.padding(12.dp),\n                    text = \"${\n                        if (startTime + move > totalTime) {\n                            totalTime.formatHourMinSec()\n                        } else {\n                            (startTime + move).formatHourMinSec()\n                        }\n                    }/${totalTime.formatHourMinSec()}\",\n                    color = Color.White\n                )\n            }\n        }\n    }\n}\n\n@Preview(device = \"spec:parent=pixel_5,orientation=landscape\")\n@Composable\nprivate fun SeekMoveTipPreview() {\n    MaterialTheme {\n        Surface {\n            SeekMoveTip(\n                show = true,\n                startTime = 2345L,\n                move = 20L,\n                totalTime = 23456L\n            )\n        }\n    }\n}\n\n@Composable\nfun QuickDoubleSpeedPlaybackTip(\n    modifier: Modifier = Modifier,\n    show: Boolean\n) {\n    if (show) {\n        Box(\n            modifier = modifier\n                .fillMaxSize()\n        ) {\n            Surface(\n                modifier = Modifier\n                    .align(Alignment.Center),\n                color = Color.Black.copy(alpha = 0.4f),\n                shape = MaterialTheme.shapes.medium\n\n            ) {\n                Text(\n                    modifier = Modifier.padding(12.dp),\n                    text = \"x2 倍速播放中\",\n                    color = Color.White\n                )\n            }\n        }\n    }\n}\n\n@Preview(device = \"spec:parent=pixel_5,orientation=landscape\")\n@Composable\nprivate fun QuickDoubleSpeedPlaybackTipPreview() {\n    MaterialTheme {\n        Surface {\n            QuickDoubleSpeedPlaybackTip(show = true)\n        }\n    }\n}\n\n@Composable\nfun BrightnessTip(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    progress: Float,\n) {\n    val scope = rememberCoroutineScope()\n\n    val displayProgress by animateFloatAsState(\n        targetValue = progress,\n        label = \"BrightnessTipProgress\"\n    )\n    val showValue by rememberUpdatedState(show)\n    var showTip by remember { mutableStateOf(false) }\n\n    LaunchedEffect(showValue) {\n        if (!showValue) {\n            scope.launch(Dispatchers.Default) {\n                delay(500)\n                if (!showValue) showTip = false\n            }\n        } else {\n            showTip = true\n        }\n    }\n\n    if (showTip) {\n        Box(\n            modifier = modifier\n                .fillMaxSize()\n        ) {\n            Surface(\n                modifier = Modifier\n                    .align(Alignment.Center),\n                color = Color.Black.copy(alpha = 0.4f),\n                shape = MaterialTheme.shapes.medium\n\n            ) {\n                Row(\n                    modifier = Modifier\n                        .padding(horizontal = 12.dp)\n                        .height(48.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    Icon(\n                        imageVector = when {\n                            progress < 1 / 3f -> Icons.Rounded.BrightnessLow\n                            progress < 2 / 3f -> Icons.Rounded.BrightnessMedium\n                            else -> Icons.Rounded.BrightnessHigh\n                        },\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                    LinearProgressIndicator(\n                        modifier = Modifier.width(100.dp),\n                        progress = { displayProgress },\n                        color = Color.White,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Preview(device = \"spec:parent=pixel_5,orientation=landscape\")\n@Composable\nprivate fun BrightnessTipPreview() {\n    MaterialTheme {\n        Surface {\n            BrightnessTip(show = true, progress = 0.3f)\n        }\n    }\n}\n\n@Composable\nfun VolumeTip(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    progress: Float,\n) {\n    val scope = rememberCoroutineScope()\n\n    val displayProgress by animateFloatAsState(\n        targetValue = progress,\n        label = \"VolumeTipProgress\"\n    )\n    val showValue by rememberUpdatedState(show)\n    var showTip by remember { mutableStateOf(false) }\n\n    LaunchedEffect(showValue) {\n        if (!showValue) {\n            scope.launch(Dispatchers.Default) {\n                delay(500)\n                if (!showValue) showTip = false\n            }\n        } else {\n            showTip = true\n        }\n    }\n\n    if (showTip) {\n        Box(\n            modifier = modifier\n                .fillMaxSize()\n        ) {\n            Surface(\n                modifier = Modifier\n                    .align(Alignment.Center),\n                color = Color.Black.copy(alpha = 0.4f),\n                shape = MaterialTheme.shapes.medium\n\n            ) {\n                Row(\n                    modifier = Modifier\n                        .padding(horizontal = 12.dp)\n                        .height(48.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    Icon(\n                        imageVector = when {\n                            progress == 0f -> Icons.Rounded.VolumeOff\n                            progress < 0.5f -> Icons.Rounded.VolumeDown\n                            else -> Icons.Rounded.VolumeUp\n                        },\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                    LinearProgressIndicator(\n                        modifier = Modifier.width(100.dp),\n                        progress = { displayProgress },\n                        color = Color.White,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Preview(device = \"spec:parent=pixel_5,orientation=landscape\")\n@Composable\nprivate fun VolumeTipPreview() {\n    MaterialTheme {\n        Surface {\n            VolumeTip(show = true, progress = 0.3f)\n        }\n    }\n}\n"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/controller/menu/DanmakuMenu.kt",
    "content": "package dev.aaa1115910.bv.player.mobile.controller.menu\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.entity.DanmakuType\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.mobile.MaterialDarkTheme\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun DanmakuMenu(\n    modifier: Modifier = Modifier,\n    onEnabledDanmakuTypeChange: (List<DanmakuType>) -> Unit,\n    onDanmakuScaleChange: (Float) -> Unit,\n    onDanmakuOpacityChange: (Float) -> Unit,\n    onDanmakuAreaChange: (Float) -> Unit,\n    onClose: () -> Unit\n) {\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            TopAppBar(\n                title = { Text(text = \"弹幕设置\") },\n                actions = {\n                    IconButton(onClick = onClose) {\n                        Icon(\n                            imageVector = Icons.Default.Close,\n                            contentDescription = null\n                        )\n                    }\n                },\n                windowInsets = WindowInsets(0, 0, 0, 0)\n            )\n        }\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .padding(top = innerPadding.calculateTopPadding())\n                .fillMaxSize(),\n            contentPadding = PaddingValues(vertical = 12.dp, horizontal = 24.dp),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            item {\n                EnabledDanmakuType(\n                    enabledDanmakuTypes = videoPlayerConfigData.currentDanmakuEnabledList,\n                    onEnabledDanmakuTypeChange = onEnabledDanmakuTypeChange\n                )\n            }\n            item {\n                DanmakuOpacity(\n                    danmakuOpacity = videoPlayerConfigData.currentDanmakuOpacity,\n                    onDanmakuOpacityChange = onDanmakuOpacityChange\n                )\n            }\n            item {\n                DanmakuArea(\n                    danmakuArea = videoPlayerConfigData.currentDanmakuArea,\n                    onDanmakuAreaChange = onDanmakuAreaChange\n                )\n            }\n            item {\n                DanmakuScale(\n                    danmakuScale = videoPlayerConfigData.currentDanmakuScale,\n                    onDanmakuScaleChange = onDanmakuScaleChange\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun EnabledDanmakuType(\n    modifier: Modifier = Modifier,\n    enabledDanmakuTypes: List<DanmakuType>,\n    onEnabledDanmakuTypeChange: (List<DanmakuType>) -> Unit\n) {\n    val onClickEnabledDanmakuTypeButton: (DanmakuType, Boolean) -> Unit = { danmakuType, blocked ->\n        val newEnabledDanmakuTypes = enabledDanmakuTypes.toMutableList()\n        newEnabledDanmakuTypes.remove(DanmakuType.All)\n        if (!blocked) {\n            newEnabledDanmakuTypes.add(danmakuType)\n        } else {\n            newEnabledDanmakuTypes.remove(danmakuType)\n        }\n        if (newEnabledDanmakuTypes.size == 3) {\n            newEnabledDanmakuTypes.add(DanmakuType.All)\n        }\n        onEnabledDanmakuTypeChange(newEnabledDanmakuTypes)\n    }\n\n    Column(\n        modifier = modifier\n    ) {\n        Text(\n            text = \"屏蔽类型\",\n            style = MaterialTheme.typography.titleSmall\n        )\n        Row(\n            modifier = Modifier\n                .fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceAround\n        ) {\n            EnabledDanmakuTypeButton(\n                danmakuType = DanmakuType.Top,\n                selected = !enabledDanmakuTypes.contains(DanmakuType.Top),\n                onEnabledStateChange = { onClickEnabledDanmakuTypeButton(DanmakuType.Top, it) }\n            )\n            EnabledDanmakuTypeButton(\n                danmakuType = DanmakuType.Bottom,\n                selected = !enabledDanmakuTypes.contains(DanmakuType.Bottom),\n                onEnabledStateChange = { onClickEnabledDanmakuTypeButton(DanmakuType.Bottom, it) }\n            )\n            EnabledDanmakuTypeButton(\n                danmakuType = DanmakuType.Rolling,\n                selected = !enabledDanmakuTypes.contains(DanmakuType.Rolling),\n                onEnabledStateChange = { onClickEnabledDanmakuTypeButton(DanmakuType.Rolling, it) }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun EnabledDanmakuTypeButton(\n    modifier: Modifier = Modifier,\n    danmakuType: DanmakuType,\n    selected: Boolean,\n    onEnabledStateChange: (Boolean) -> Unit\n) {\n    val context = LocalContext.current\n\n    FilterChip(\n        modifier = modifier,\n        label = { Text(text = danmakuType.getDisplayName(context).replace(\"弹幕\", \"\")) },\n        selected = selected,\n        onClick = { onEnabledStateChange(!selected) }\n    )\n}\n\n@Composable\nprivate fun DanmakuOpacity(\n    modifier: Modifier = Modifier,\n    danmakuOpacity: Float,\n    onDanmakuOpacityChange: (Float) -> Unit\n) {\n    Column(\n        modifier = modifier\n    ) {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Text(\n                text = \"不透明度\",\n                style = MaterialTheme.typography.titleSmall\n            )\n            Text(\n                text = \"${(danmakuOpacity * 100).toInt()}%\",\n                style = MaterialTheme.typography.titleSmall\n            )\n        }\n\n        Slider(value = danmakuOpacity, onValueChange = onDanmakuOpacityChange)\n    }\n}\n\n@Composable\nprivate fun DanmakuArea(\n    modifier: Modifier = Modifier,\n    danmakuArea: Float,\n    onDanmakuAreaChange: (Float) -> Unit\n) {\n    Column(\n        modifier = modifier\n    ) {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Text(\n                text = \"显示区域\",\n                style = MaterialTheme.typography.titleSmall\n            )\n            Text(\n                text = \"${(danmakuArea * 100).toInt()}%\",\n                style = MaterialTheme.typography.titleSmall\n            )\n        }\n\n        Slider(value = danmakuArea, onValueChange = onDanmakuAreaChange)\n    }\n}\n\n@Composable\nprivate fun DanmakuScale(\n    modifier: Modifier = Modifier,\n    danmakuScale: Float,\n    onDanmakuScaleChange: (Float) -> Unit\n) {\n    Column(\n        modifier = modifier\n    ) {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Text(\n                text = \"字体缩放\",\n                style = MaterialTheme.typography.titleSmall\n            )\n            Text(\n                text = \"${(danmakuScale * 100).toInt()}%\",\n                style = MaterialTheme.typography.titleSmall\n            )\n        }\n\n        Slider(\n            value = danmakuScale,\n            onValueChange = onDanmakuScaleChange,\n            valueRange = 0.5f..2f,\n        )\n    }\n}\n\n@Preview(device = \"spec:width=300dp,height=400dp,dpi=440\")\n@Composable\nprivate fun ResolutionMenuPreview() {\n    MaterialDarkTheme {\n        DanmakuMenu(\n            onEnabledDanmakuTypeChange = {},\n            onDanmakuScaleChange = {},\n            onDanmakuOpacityChange = {},\n            onDanmakuAreaChange = {},\n            onClose = {}\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun EnabledDanmakuTypePreview() {\n    MaterialDarkTheme {\n        Surface {\n            EnabledDanmakuType(\n                enabledDanmakuTypes = listOf(DanmakuType.Bottom),\n                onEnabledDanmakuTypeChange = {}\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun DanmakuOpacityPreview() {\n    MaterialDarkTheme {\n        Surface {\n            DanmakuOpacity(\n                danmakuOpacity = 0.6f,\n                onDanmakuOpacityChange = {}\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun DanmakuAreaPreview() {\n    MaterialDarkTheme {\n        Surface {\n            DanmakuArea(\n                danmakuArea = 0.6f,\n                onDanmakuAreaChange = {}\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun DanmakuScalePreview() {\n    MaterialDarkTheme {\n        Surface {\n            DanmakuScale(\n                danmakuScale = 1f,\n                onDanmakuScaleChange = {}\n            )\n        }\n    }\n}"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/controller/menu/DashMenu.kt",
    "content": "package dev.aaa1115910.bv.player.mobile.controller.menu\n\nimport android.annotation.SuppressLint\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ExperimentalLayoutApi\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Done\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FilterChipDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.player.entity.VideoPlayerConfigData\nimport dev.aaa1115910.bv.player.mobile.MaterialDarkTheme\n\n@SuppressLint(\"UnusedMaterial3ScaffoldPaddingParameter\")\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun DashMenu(\n    modifier: Modifier = Modifier,\n    onChangeResolution: (Resolution) -> Unit,\n    onChangeVideoCodec: (VideoCodec) -> Unit,\n    onChangeAudio: (Audio) -> Unit,\n    onClose: () -> Unit\n) {\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            TopAppBar(\n                title = { Text(text = \"音频画质\") },\n                actions = {\n                    IconButton(onClick = onClose) {\n                        Icon(\n                            imageVector = Icons.Default.Close,\n                            contentDescription = null\n                        )\n                    }\n                },\n                windowInsets = WindowInsets(0, 0, 0, 0)\n            )\n        }\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .padding(top = innerPadding.calculateTopPadding())\n                .padding(horizontal = 18.dp),\n            contentPadding = PaddingValues(bottom = 32.dp)\n        ) {\n            item {\n                ResolutionContent(\n                    onClickResolution = onChangeResolution,\n                    onClickCodec = onChangeVideoCodec\n                )\n            }\n            item {\n                AudioContent(\n                    onClickAudio = onChangeAudio\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalLayoutApi::class)\n@Composable\nprivate fun ResolutionContent(\n    modifier: Modifier = Modifier,\n    onClickResolution: (Resolution) -> Unit,\n    onClickCodec: (VideoCodec) -> Unit\n) {\n    val context = LocalContext.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n\n    Column(\n        modifier = modifier\n    ) {\n        Text(text = \"视频清晰度\")\n        FlowRow(\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n        ) {\n            videoPlayerConfigData.availableResolutions\n                .sortedByDescending { it.code }\n                .forEach { resolution ->\n                    ChipItem(\n                        text = resolution.getDisplayName(context),\n                        selected = videoPlayerConfigData.currentResolution == resolution,\n                        onClick = {\n                            println(\"click resolution item: $resolution\")\n                            onClickResolution(resolution)\n                        }\n                    )\n                }\n        }\n\n        Text(text = \"视频编码\")\n        FlowRow(\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n        ) {\n            videoPlayerConfigData.availableVideoCodec.forEach { codec ->\n                ChipItem(\n                    text = codec.getDisplayName(context),\n                    selected = videoPlayerConfigData.currentVideoCodec == codec,\n                    onClick = {\n                        println(\"click codec item: $codec\")\n                        onClickCodec(codec)\n                    }\n                )\n            }\n        }\n\n        /*\n        Text(text = \"画面线路\")\n        FlowRow(\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n        ) {\n            listOf(\"线路1\", \"线路2\", \"线路3\").forEach { name ->\n                ChipItem(\n                    text = name,\n                    selected = false,\n                    onClick = { \"NOT IMPLEMENTED\".toast(context) }\n                )\n            }\n        }*/\n    }\n}\n\n@OptIn(ExperimentalLayoutApi::class)\n@Composable\nfun AudioContent(\n    modifier: Modifier = Modifier,\n    onClickAudio: (Audio) -> Unit\n) {\n    val context = LocalContext.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n\n    Column(\n        modifier = modifier\n    ) {\n        Text(text = \"音频采样率\")\n        FlowRow(\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n        ) {\n            videoPlayerConfigData.availableAudio\n                .sortedByDescending { it.ordinal }\n                .forEach { audio ->\n                    ChipItem(\n                        text = audio.getDisplayName(context),\n                        selected = videoPlayerConfigData.currentAudio == audio,\n                        onClick = {\n                            println(\"click audio item: $audio\")\n                            onClickAudio(audio)\n                        }\n                    )\n                }\n        }\n\n        /* Text(text = \"音频线路\")\n         FlowRow(\n             horizontalArrangement = Arrangement.spacedBy(8.dp),\n         ) {\n             listOf(\"线路1\", \"线路2\", \"线路3\").forEach { name ->\n                 ChipItem(\n                     text = name,\n                     selected = false,\n                     onClick = { \"NOT IMPLEMENTED\".toast(context) }\n                 )\n             }\n         }*/\n    }\n}\n\n@Composable\nprivate fun ChipItem(\n    modifier: Modifier = Modifier,\n    text: String,\n    selected: Boolean,\n    onClick: () -> Unit = {}\n) {\n    FilterChip(\n        modifier = modifier,\n        selected = selected,\n        label = {\n            Text(\n                text = text,\n                maxLines = 1\n            )\n        },\n        leadingIcon = (@Composable {\n            Icon(\n                imageVector = Icons.Filled.Done,\n                contentDescription = null,\n                modifier = Modifier.size(FilterChipDefaults.IconSize)\n            )\n        }).takeIf { selected },\n        onClick = onClick\n    )\n}\n\n@Preview\n@Composable\nprivate fun ResolutionListItemSelectedPreview() {\n    MaterialTheme {\n        ChipItem(\n            text = \"1080P 60FPS\",\n            selected = true\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun ResolutionListItemUnselectedPreview() {\n    MaterialTheme {\n        ChipItem(\n            text = \"1080P 60FPS\",\n            selected = false\n        )\n    }\n}\n\n@Preview(device = \"spec:width=300dp,height=400dp,dpi=440\")\n@Composable\nprivate fun ResolutionMenuPreview() {\n    MaterialDarkTheme {\n        CompositionLocalProvider(\n            LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n                currentResolution = Resolution.R720P,\n                currentAudio = Audio.A132K,\n                availableResolutions = Resolution.entries,\n                availableAudio = Audio.entries,\n                availableVideoCodec = VideoCodec.entries\n            )\n        ) {\n            DashMenu(\n                onChangeResolution = {},\n                onChangeVideoCodec = {},\n                onChangeAudio = {},\n                onClose = {}\n            )\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun ResolutionContentPreview() {\n    var currentResolution by remember { mutableStateOf(Resolution.R1080P) }\n    var currentVideoCodec by remember { mutableStateOf(VideoCodec.HEVC) }\n\n    MaterialDarkTheme {\n        CompositionLocalProvider(\n            LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n                currentResolution = currentResolution,\n                availableResolutions = Resolution.entries,\n                availableVideoCodec = VideoCodec.entries\n            )\n        ) {\n            Surface {\n                ResolutionContent(\n                    onClickResolution = { currentResolution = it },\n                    onClickCodec = { currentVideoCodec = it },\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/controller/menu/MoreMenu.kt",
    "content": "package dev.aaa1115910.bv.player.mobile.controller.menu\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.PlayMode.ListOrder\nimport dev.aaa1115910.bv.player.entity.PlayMode.ListOrderReverse\nimport dev.aaa1115910.bv.player.entity.PlayMode.SingleLoop\nimport dev.aaa1115910.bv.player.entity.PlayMode.SingleVideo\nimport dev.aaa1115910.bv.player.mobile.MaterialDarkTheme\n\nprivate val MobileSupportedPlayModes = listOf(\n    SingleVideo,\n    SingleLoop,\n    ListOrder,\n    ListOrderReverse\n)\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun MoreMenu(\n    modifier: Modifier = Modifier,\n    onClose: () -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit\n) {\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            TopAppBar(\n                title = { Text(text = \"更多设置\") },\n                actions = {\n                    IconButton(onClick = onClose) {\n                        Icon(\n                            imageVector = Icons.Default.Close,\n                            contentDescription = null\n                        )\n                    }\n                },\n                windowInsets = WindowInsets(0, 0, 0, 0)\n            )\n        }\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .padding(top = innerPadding.calculateTopPadding())\n                .fillMaxSize(),\n            contentPadding = PaddingValues(vertical = 12.dp, horizontal = 24.dp),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            item {\n                PlayModeContent(\n                    modifier = Modifier,\n                    playMode = videoPlayerConfigData.currentPlayMode,\n                    onPlayModeChange = onPlayModeChange\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun PlayModeContent(\n    modifier: Modifier = Modifier,\n    playMode: PlayMode,\n    onPlayModeChange: (PlayMode) -> Unit\n) {\n    val context = LocalContext.current\n\n    Column(\n        modifier = modifier\n    ) {\n\n        Text(\n            text = \"播放模式\",\n            style = MaterialTheme.typography.titleSmall\n        )\n        FlowRow(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n        ) {\n            MobileSupportedPlayModes.forEach {\n                FilterChip(\n                    modifier = modifier,\n                    label = { Text(text = it.getDisplayName(context)) },\n                    selected = playMode == it,\n                    onClick = { onPlayModeChange(it) }\n                )\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun MoreMenuPreview() {\n    MaterialDarkTheme {\n        MoreMenu(\n            onClose = {},\n            onPlayModeChange = {}\n        )\n    }\n}\n"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/controller/menu/SpeedMenu.kt",
    "content": "package dev.aaa1115910.bv.player.mobile.controller.menu\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.mobile.MaterialDarkTheme\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SpeedMenu(\n    modifier: Modifier = Modifier,\n    onClickSpeed: (Float) -> Unit,\n    onClose: () -> Unit\n) {\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val currentSpeed = videoPlayerConfigData.currentVideoSpeed\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            TopAppBar(\n                title = { Text(text = \"播放速度\") },\n                actions = {\n                    IconButton(onClick = onClose) {\n                        Icon(\n                            imageVector = Icons.Default.Close,\n                            contentDescription = null\n                        )\n                    }\n                },\n                windowInsets = WindowInsets(0, 0, 0, 0)\n            )\n        }\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .padding(top = innerPadding.calculateTopPadding())\n                .fillMaxSize(),\n            contentPadding = PaddingValues(vertical = 12.dp),\n            verticalArrangement = Arrangement.Center,\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            items(\n                items = availableSpeedList\n                    .toList()\n                    .sortedByDescending { it.first }\n            ) { (speedNum, speedName) ->\n                SpeedListItem(\n                    text = speedName,\n                    selected = currentSpeed == speedNum,\n                    onClick = {\n                        println(\"click speed menu: $speedName($speedNum)\")\n                        onClickSpeed(speedNum)\n                    }\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SpeedListItem(\n    modifier: Modifier = Modifier,\n    text: String,\n    selected: Boolean,\n    onClick: () -> Unit = {}\n) {\n    val textColor = if (selected) MaterialTheme.colorScheme.primary else Color.White\n\n    Surface(\n        modifier = modifier\n            .size(120.dp, 48.dp),\n        onClick = onClick,\n        color = Color.Transparent\n    ) {\n        Row(\n            modifier = Modifier.fillMaxSize(),\n            horizontalArrangement = Arrangement.Start,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Text(\n                modifier = Modifier\n                    .padding(start = 32.dp),\n                text = text,\n                color = textColor\n            )\n        }\n    }\n}\n\nprivate val availableSpeedList = mapOf(\n    2.0f to \"2x\",\n    1.75f to \"1.75x\",\n    1.5f to \"1.5x\",\n    1.25f to \"1.25x\",\n    1.0f to \"1.0x\",\n    0.75f to \"0.75x\",\n    0.5f to \"0.5x\"\n)\n\n@Preview\n@Composable\nprivate fun SpeedListItemSelectedPreview() {\n    MaterialTheme {\n        SpeedListItem(\n            text = \"1.0x\",\n            selected = true\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun SpeedListItemUnselectedPreview() {\n    MaterialTheme {\n        SpeedListItem(\n            text = \"1.0x\",\n            selected = false\n        )\n    }\n}\n\n@Preview(device = \"spec:width=300dp,height=400dp,dpi=440\")\n@Composable\nprivate fun ResolutionMenuPreview() {\n    MaterialDarkTheme {\n        SpeedMenu(\n            onClickSpeed = {},\n            onClose = {}\n        )\n    }\n}\n"
  },
  {
    "path": "player/mobile/src/main/kotlin/dev/aaa1115910/bv/player/mobile/controller/menu/VideoListMenu.kt",
    "content": "package dev.aaa1115910.bv.player.mobile.controller.menu\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.contentColorFor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.VideoListItem\nimport dev.aaa1115910.bv.player.entity.VideoListPart\nimport dev.aaa1115910.bv.player.entity.VideoListPgcEpisode\nimport dev.aaa1115910.bv.player.entity.VideoListUgcEpisode\nimport dev.aaa1115910.bv.player.entity.VideoListUgcEpisodeTitle\nimport dev.aaa1115910.bv.player.entity.VideoPlayerConfigData\nimport dev.aaa1115910.bv.player.mobile.MaterialDarkTheme\nimport dev.aaa1115910.bv.util.ifElse\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun VideoListMenu(\n    modifier: Modifier = Modifier,\n    onClickVideoListItem: (VideoListItem) -> Unit,\n    onClose: () -> Unit\n) {\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val list = videoPlayerConfigData.availableVideoList\n    val selectedVideoListItem by remember(videoPlayerConfigData.currentVideoCid) {\n        derivedStateOf {\n            list.first {\n                when (it) {\n                    is VideoListPart -> it.cid == videoPlayerConfigData.currentVideoCid\n                    is VideoListUgcEpisode -> it.cid == videoPlayerConfigData.currentVideoCid\n                    is VideoListPgcEpisode -> it.cid == videoPlayerConfigData.currentVideoCid\n                    else -> false\n                }\n            }\n        }\n    }\n    val isUgcSeason by remember {\n        derivedStateOf {\n            videoPlayerConfigData.availableVideoList.any { it is VideoListUgcEpisode }\n        }\n    }\n\n    Scaffold(\n        modifier = modifier,\n        topBar = {\n            TopAppBar(\n                title = { Text(text = \"播放列表\") },\n                actions = {\n                    IconButton(onClick = onClose) {\n                        Icon(\n                            imageVector = Icons.Default.Close,\n                            contentDescription = null\n                        )\n                    }\n                },\n                windowInsets = WindowInsets(0, 0, 0, 0)\n            )\n        }\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .padding(top = innerPadding.calculateTopPadding())\n                .fillMaxSize(),\n            contentPadding = PaddingValues(vertical = 12.dp, horizontal = 12.dp),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            items(items = list) { item ->\n                VideoListItem(\n                    item = item,\n                    selected = item == selectedVideoListItem,\n                    inUgcEpisode = isUgcSeason,\n                    onClick = onClickVideoListItem\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun VideoListItem(\n    modifier: Modifier = Modifier,\n    item: VideoListItem,\n    selected: Boolean,\n    inUgcEpisode: Boolean,\n    onClick: (VideoListItem) -> Unit\n) {\n    val textPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp)\n    Surface(\n        modifier = modifier\n            .fillMaxWidth()\n            .clip(MaterialTheme.shapes.small)\n            .ifElse({ item !is VideoListUgcEpisodeTitle }, Modifier.clickable { onClick(item) }),\n        color = if (selected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent,\n        contentColor = if (selected) contentColorFor(MaterialTheme.colorScheme.primaryContainer)\n        else Color.White.copy(alpha = 0.9f)\n    ) {\n        when (item) {\n            is VideoListPart -> {\n                Text(\n                    text = (\" - \".takeIf { inUgcEpisode }\n                        ?: \"\") + \"P${item.index + 1} ${item.title}\",\n                    modifier = modifier\n                        .padding(textPadding),\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis\n                )\n            }\n\n            is VideoListUgcEpisode -> {\n                Text(\n                    text = \"EP${item.index + 1} ${item.title}\",\n                    modifier = modifier\n                        .padding(textPadding),\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis\n                )\n            }\n\n            is VideoListPgcEpisode -> {\n                Text(\n                    text = item.title,\n                    modifier = modifier\n                        .padding(textPadding),\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis\n                )\n            }\n\n            is VideoListUgcEpisodeTitle -> {\n                Text(\n                    text = \"EP${item.index + 1} ${item.title}\",\n                    modifier = modifier\n                        .padding(textPadding),\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis\n                )\n            }\n        }\n    }\n}\n\n\n@Preview\n@Composable\nprivate fun VideoListItemPreview() {\n    MaterialDarkTheme {\n        VideoListItem(\n            item = VideoListPart(\n                aid = 0,\n                cid = 0,\n                title = \"This is title\",\n                index = 2\n            ),\n            selected = false,\n            inUgcEpisode = false,\n            onClick = {}\n        )\n    }\n}\n\n@Preview(device = \"spec:width=300dp,height=400dp,dpi=440\")\n@Composable\nprivate fun VideoListMenuContentNormalPartPreview() {\n    CompositionLocalProvider(\n        LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n            availableVideoList = List(20) {\n                VideoListPart(\n                    aid = it.toLong(),\n                    cid = it.toLong(),\n                    title = \"This is title $it\",\n                    index = it\n                )\n            },\n            currentVideoCid = 3\n        )\n    ) {\n        MaterialDarkTheme {\n            VideoListMenu(\n                onClickVideoListItem = {},\n                onClose = {}\n            )\n        }\n    }\n}\n\n@Preview(device = \"spec:width=300dp,height=400dp,dpi=440\")\n@Composable\nprivate fun VideoListMenuContentPgcSeasonPreview() {\n    CompositionLocalProvider(\n        LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n            availableVideoList = List(20) {\n                VideoListPgcEpisode(\n                    aid = it.toLong(),\n                    cid = it.toLong(),\n                    title = \"This is title $it\",\n                    index = it\n                )\n            },\n            currentVideoCid = 3\n        )\n    ) {\n        MaterialDarkTheme {\n            VideoListMenu(\n                onClickVideoListItem = {},\n                onClose = {}\n            )\n        }\n    }\n}\n\n@Preview(device = \"spec:width=300dp,height=400dp,dpi=440\")\n@Composable\nprivate fun VideoListMenuContentUgcSeasonPreview() {\n    CompositionLocalProvider(\n        LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n            availableVideoList = listOf(\n                *(0..1).map {\n                    VideoListUgcEpisode(\n                        aid = it.toLong(),\n                        cid = it.toLong(),\n                        title = \"This is title for ep ${it + 1}\",\n                        index = it\n                    )\n                }.toTypedArray(),\n                VideoListUgcEpisodeTitle(\n                    title = \"This is title for ep 3\",\n                    index = 2\n                ),\n                *(0..4).map {\n                    VideoListPart(\n                        aid = it.toLong(),\n                        cid = it.toLong(),\n                        title = \"part $it in ep3\",\n                        index = it\n                    )\n                }.toTypedArray(),\n                *(3..5).map {\n                    VideoListUgcEpisode(\n                        aid = it.toLong(),\n                        cid = it.toLong(),\n                        title = \"This is title for ep ${it + 1}\",\n                        index = it\n                    )\n                }.toTypedArray()\n            ),\n            currentVideoCid = 3\n        )\n    ) {\n        MaterialDarkTheme {\n            VideoListMenu(\n                onClickVideoListItem = {},\n                onClose = {}\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "player/shared/.gitignore",
    "content": "/build"
  },
  {
    "path": "player/shared/build.gradle.kts",
    "content": "plugins {\n    alias(gradleLibs.plugins.android.library)\n    alias(gradleLibs.plugins.compose.compiler)\n    alias(gradleLibs.plugins.kotlin.android)\n}\n\nandroid {\n    namespace = \"${AppConfiguration.appId}.player.shared\"\n    compileSdk = AppConfiguration.compileSdk\n\n    defaultConfig {\n        minSdk = AppConfiguration.minSdk\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles(\"consumer-rules.pro\")\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"r8Test\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"alpha\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n\n    buildFeatures {\n        compose = true\n        buildConfig = true\n    }\n\n    testOptions {\n        targetSdk = AppConfiguration.targetSdk\n    }\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\ndependencies {\n    api(project(\":utils\"))\n    api(project(\":bili-api\"))\n    api(project(\":bili-subtitle\"))\n    api(project(\":symbols\"))\n    implementation(androidx.activity.compose)\n    implementation(androidx.core.ktx)\n    implementation(androidx.compose.constraintlayout)\n    implementation(androidx.compose.material)\n    implementation(androidx.compose.material.icons)\n    implementation(androidx.compose.material3)\n    implementation(androidx.compose.tv.foundation)\n    implementation(androidx.compose.tv.material)\n    implementation(androidx.compose.ui)\n    implementation(androidx.compose.ui.tooling.preview)\n    implementation(androidx.compose.ui.util)\n    implementation(libs.androidSvg)\n    implementation(libs.logging)\n    implementation(libs.lottie)\n    implementation(libs.material)\n    debugImplementation(androidx.compose.ui.test.manifest)\n    debugImplementation(androidx.compose.ui.tooling)\n}"
  },
  {
    "path": "player/shared/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "player/shared/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "player/shared/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/CacheManager.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku\n\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.graphics.Typeface\nimport android.os.Handler\nimport android.os.HandlerThread\nimport android.os.Looper\nimport android.os.Message\nimport android.os.Process\nimport dev.aaa1115910.bv.player.danmaku.model.DanmakuCacheState\nimport dev.aaa1115910.bv.player.danmaku.model.DanmakuItem\nimport java.util.concurrent.ConcurrentLinkedQueue\nimport java.util.concurrent.atomic.AtomicInteger\nimport kotlin.math.ceil\nimport kotlin.math.max\nimport kotlin.math.min\nimport kotlin.math.roundToInt\n\ninternal class CacheStyle(\n    val textSizePx: Float,\n    val textSizeScale: Int,\n    val fontWeight: DanmakuFontWeight,\n    val strokeWidthPx: Float,\n    val outlinePadPx: Float,\n    val generation: Int,\n)\n\ninternal class CacheManager(\n    private val mainLooper: Looper,\n    private val onRenderSign: () -> Unit,\n) {\n    private val mainHandler = Handler(mainLooper)\n    private val thread = HandlerThread(\"Danmaku-Cache\").apply {\n        start()\n        try { Process.setThreadPriority(threadId, Process.THREAD_PRIORITY_BACKGROUND) } catch (_: Exception) {}\n    }\n    private val handler: Handler = CacheHandler(thread.looper)\n    private val pool = BitmapPool(maxBytes = CACHE_POOL_MAX_BYTES, maxCount = CACHE_POOL_MAX_COUNT)\n    private val queueDepth = AtomicInteger(0)\n    private val releaseQueue = ConcurrentLinkedQueue<PendingRelease>()\n    @Volatile private var released = false\n\n    // Cache paint (cache thread only)\n    private val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        typeface = Typeface.DEFAULT_BOLD\n        isSubpixelText = true\n    }\n    private val stroke = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        typeface = Typeface.DEFAULT_BOLD\n        style = Paint.Style.STROKE\n        isSubpixelText = true\n    }\n    private val fontMetrics = Paint.FontMetrics()\n    private var cachedFontMetricsTextSize: Float = Float.NaN\n    private var cachedPaintTextSize: Float = Float.NaN\n    private var cachedStrokeWidth: Float = Float.NaN\n    private val cacheCanvas = Canvas()\n    private val renderSignRunnable = Runnable { onRenderSign() }\n\n    fun queueDepth(): Int = queueDepth.get().coerceAtLeast(0)\n\n    fun requestBuildCache(item: DanmakuItem, textWidthPx: Float, style: CacheStyle, releaseAtFrameId: Int) {\n        queueDepth.incrementAndGet()\n        handler.obtainMessage(MSG_BUILD_CACHE, CacheRequest(item, textWidthPx, style, releaseAtFrameId)).sendToTarget()\n    }\n\n    fun enqueueRelease(bitmap: Bitmap?, releaseAtFrameId: Int) {\n        if (bitmap == null || bitmap.isRecycled) return\n        releaseQueue.add(PendingRelease(bitmap, releaseAtFrameId))\n    }\n\n    fun drainReleasedBitmaps(currentFrameId: Int) {\n        var drained = 0\n        while (drained < MAX_RELEASE_PER_DRAIN) {\n            val head = releaseQueue.peek() ?: break\n            if (head.releaseAtFrameId > currentFrameId) break\n            releaseQueue.poll()\n            drained++\n            val bmp = head.bitmap\n            if (bmp.isRecycled) continue\n            if (!pool.tryPut(bmp)) try { bmp.recycle() } catch (_: Exception) {}\n        }\n    }\n\n    fun clear() {\n        handler.removeCallbacksAndMessages(null)\n        handler.sendEmptyMessage(MSG_CLEAR)\n    }\n\n    fun release() {\n        released = true\n        handler.removeCallbacksAndMessages(null)\n        handler.sendEmptyMessage(MSG_RELEASE)\n    }\n\n    private inner class CacheHandler(looper: Looper) : Handler(looper) {\n        override fun handleMessage(msg: Message) {\n            when (msg.what) {\n                MSG_BUILD_CACHE -> {\n                    val req = msg.obj as? CacheRequest ?: return\n                    queueDepth.decrementAndGet()\n                    buildCache(req)\n                }\n                MSG_CLEAR -> {\n                    queueDepth.set(0)\n                    pool.clear()\n                    releaseQueue.clear()\n                }\n                MSG_RELEASE -> {\n                    removeCallbacksAndMessages(null)\n                    queueDepth.set(0)\n                    pool.clear()\n                    releaseQueue.clear()\n                    try { thread.quitSafely() } catch (_: Exception) {}\n                }\n            }\n        }\n    }\n\n    private fun buildCache(req: CacheRequest) {\n        if (released) return\n        val item = req.item\n        val style = req.style\n        val existing = item.cacheBitmap\n        if (existing != null && !existing.isRecycled && item.cacheGeneration == style.generation) {\n            item.cacheState = DanmakuCacheState.Rendered\n            return\n        }\n        if (style.textSizePx <= 0f || !style.textSizePx.isFinite()) return\n\n        val outlinePad = style.outlinePadPx.coerceAtLeast(0f)\n        val strokeWidth = style.strokeWidthPx.coerceAtLeast(0f)\n        val desiredTypeface = style.fontWeight.typeface\n        if (fill.typeface != desiredTypeface) {\n            fill.typeface = desiredTypeface\n            cachedFontMetricsTextSize = Float.NaN\n        }\n        if (stroke.typeface != desiredTypeface) stroke.typeface = desiredTypeface\n\n        // Compute effective font size: min(danmaku.textSize, 25) * (textSizeScale / 100)\n        val clampedSize = min(item.data.textSize, 25)\n        val scaleFactor = style.textSizeScale.coerceIn(25, 200) / 100f\n        val effectiveTextSizePx = (style.textSizePx * clampedSize / 25f * scaleFactor).coerceAtLeast(1f)\n        if (effectiveTextSizePx != cachedPaintTextSize) {\n            fill.textSize = effectiveTextSizePx\n            stroke.textSize = effectiveTextSizePx\n            cachedPaintTextSize = effectiveTextSizePx\n        }\n        if (strokeWidth != cachedStrokeWidth) {\n            stroke.strokeWidth = strokeWidth\n            cachedStrokeWidth = strokeWidth\n        }\n\n        if (effectiveTextSizePx != cachedFontMetricsTextSize) {\n            fill.getFontMetrics(fontMetrics)\n            cachedFontMetricsTextSize = effectiveTextSizePx\n        }\n        val textHeightPx = (fontMetrics.descent - fontMetrics.ascent).coerceAtLeast(1f)\n        val boxHeight = ceil(textHeightPx + outlinePad * 2f).toInt().coerceAtLeast(1)\n        val boxWidth = ceil(req.textWidthPx.coerceAtLeast(outlinePad * 2f)).toInt().coerceAtLeast(1)\n\n        val bmp = pool.acquire(boxWidth, boxHeight)\n            ?: try { Bitmap.createBitmap(boxWidth, boxHeight, Bitmap.Config.ARGB_8888) } catch (_: Exception) { null }\n            ?: return\n        bmp.eraseColor(0x00000000)\n\n        val canvas = cacheCanvas\n        canvas.setBitmap(bmp)\n        val rgb = item.data.color and 0xFFFFFF\n        stroke.color = 0xCC shl 24\n        fill.color = (0xFF shl 24) or rgb\n\n        val baseline = outlinePad - fontMetrics.ascent\n        val text = item.data.text\n        if (text.isNotBlank()) {\n            if (strokeWidth > 0.01f) canvas.drawText(text, outlinePad, baseline, stroke)\n            canvas.drawText(text, outlinePad, baseline, fill)\n        }\n\n        val old = item.cacheBitmap\n        item.cacheBitmap = bmp\n        item.cacheGeneration = style.generation\n        item.cacheState = DanmakuCacheState.Rendered\n        if (old != null && old != bmp) enqueueRelease(old, req.releaseAtFrameId)\n\n        mainHandler.post(renderSignRunnable)\n    }\n\n    private class CacheRequest(val item: DanmakuItem, val textWidthPx: Float, val style: CacheStyle, val releaseAtFrameId: Int)\n    private class PendingRelease(val bitmap: Bitmap, val releaseAtFrameId: Int)\n\n    private class BitmapPool(private val maxBytes: Long, private val maxCount: Int) {\n        private val pool = ArrayDeque<Bitmap>()\n        private var pooledBytes: Long = 0L\n\n        @Synchronized\n        fun acquire(minWidth: Int, minHeight: Int): Bitmap? {\n            val it = pool.iterator()\n            while (it.hasNext()) {\n                val b = it.next()\n                if (b.isRecycled) { it.remove(); continue }\n                if (b.width >= minWidth && b.height >= minHeight && b.width - minWidth <= 48 && b.height - minHeight <= 24 && b.config == Bitmap.Config.ARGB_8888) {\n                    it.remove()\n                    pooledBytes -= b.allocationByteCount.toLong().coerceAtLeast(0L)\n                    return b\n                }\n            }\n            return null\n        }\n\n        @Synchronized\n        fun tryPut(bitmap: Bitmap): Boolean {\n            if (bitmap.isRecycled) return true\n            val bytes = bitmap.allocationByteCount.toLong().coerceAtLeast(0L)\n            if (bytes <= 0L || bytes > maxBytes) return false\n            if (pool.size >= maxCount || pooledBytes + bytes > maxBytes) return false\n            pool.addLast(bitmap)\n            pooledBytes += bytes\n            return true\n        }\n\n        @Synchronized\n        fun clear() {\n            val it = pool.iterator()\n            while (it.hasNext()) {\n                val b = it.next(); it.remove()\n                try { if (!b.isRecycled) b.recycle() } catch (_: Exception) {}\n            }\n            pooledBytes = 0L\n        }\n    }\n\n    companion object {\n        private const val MSG_BUILD_CACHE = 2001\n        private const val MSG_CLEAR = 2002\n        private const val MSG_RELEASE = 2099\n        private const val CACHE_POOL_MAX_BYTES = 50L * 1024L * 1024L\n        private const val CACHE_POOL_MAX_COUNT = 72\n        private const val MAX_RELEASE_PER_DRAIN = 24\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/DanmakuConfig.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku\n\nimport android.graphics.Typeface\n\nenum class DanmakuLaneDensity(val laneHeightFactor: Float) {\n    Sparse(1.25f),\n    Standard(1.0f),\n    Dense(0.85f),\n}\n\nenum class DanmakuFontWeight(val typeface: Typeface) {\n    Normal(Typeface.DEFAULT),\n    Bold(Typeface.DEFAULT_BOLD),\n}\n\ndata class DanmakuConfig(\n    val enabled: Boolean = true,\n    val opacity: Float = 1f,\n    val textSizeSp: Float = 18f,\n    val textSizeScale: Int = 100,\n    val fontWeight: DanmakuFontWeight = DanmakuFontWeight.Bold,\n    val strokeWidthPx: Int = 3,\n    val durationMultiplier: Float = 1f,\n    val area: Float = 1f,\n    val laneDensity: DanmakuLaneDensity = DanmakuLaneDensity.Standard,\n    val allowScroll: Boolean = true,\n    val allowTop: Boolean = true,\n    val allowBottom: Boolean = true,\n    val minLevel: Int = 0,\n)\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/DanmakuEngine.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku\n\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.graphics.Typeface\nimport android.util.DisplayMetrics\nimport android.util.Log\nimport android.util.TypedValue\nimport dev.aaa1115910.bv.player.danmaku.model.Danmaku\nimport dev.aaa1115910.bv.player.danmaku.model.DanmakuCacheState\nimport dev.aaa1115910.bv.player.danmaku.model.DanmakuItem\nimport dev.aaa1115910.bv.player.danmaku.model.DanmakuKind\nimport dev.aaa1115910.bv.player.danmaku.model.RenderSnapshot\nimport kotlin.math.ceil\nimport kotlin.math.max\nimport kotlin.math.min\nimport kotlin.math.roundToInt\n\ninternal class DanmakuEngine(\n    private val displayMetrics: DisplayMetrics,\n    private val cacheManager: CacheManager,\n) {\n    // Data (action thread)\n    private val actionStateLock = Any()\n    private var allItems: MutableList<DanmakuItem> = mutableListOf()\n    private var items: MutableList<DanmakuItem> = mutableListOf()\n    private var index: Int = 0\n    private val active = ArrayList<DanmakuItem>(64)\n    private var lastNowMs: Double = 0.0\n\n    // Viewport / Config\n    @Volatile var viewportWidth: Int = 0; private set\n    @Volatile var viewportHeight: Int = 0; private set\n    @Volatile var viewportTopInsetPx: Int = 0; private set\n    @Volatile var viewportBottomInsetPx: Int = 0; private set\n\n    @Volatile var config: DanmakuConfig = DanmakuConfig()\n    @Volatile private var textSizePx: Float = sp(18f)\n    @Volatile private var strokeWidthPx: Float = 3f\n    @Volatile private var outlinePadPx: Float = 1.5f\n    @Volatile private var cacheStyleGeneration: Int = 0\n\n    // Time (main → action)\n    @Volatile private var currentPositionMs: Double = 0.0\n    @Volatile private var currentUiFrameId: Int = 0\n\n    // Snapshot double buffer\n    private val snapshotA = RenderSnapshot()\n    private val snapshotB = RenderSnapshot()\n    @Volatile private var latestSnapshot: RenderSnapshot = snapshotA\n    private var snapshotDirty: Boolean = true\n\n    // FPS stats (action thread)\n    private var actFrameCount: Int = 0\n    private var actDroppedFrames: Int = 0\n    private var actLastLogNanos: Long = System.nanoTime()\n    private var actLastFrameNanos: Long = 0L\n    private val actFrameDeadlineNanos: Long = 16_666_667L // ~60fps\n    private var actStartNanos: Long = 0L\n    private var actDurationTotalNanos: Long = 0L\n    private var actDurationMaxNanos: Long = 0L\n    private var actDurationSamples: LongArray = LongArray(128)\n    private var actDurationSampleCount: Int = 0\n\n    // Layout scratch (action thread)\n    private val actionFontMetrics = Paint.FontMetrics()\n    private val actionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Typeface.DEFAULT_BOLD }\n    private var scrollLaneQueues: Array<ArrayDeque<DanmakuItem>> = emptyArray()\n    private var topLaneBusyUntilMs = DoubleArray(0)\n    private var bottomLaneBusyUntilMs = DoubleArray(0)\n    private var cacheProbeCursor: Int = 0\n    private var cachedCacheStyle: CacheStyle? = null\n\n    // Cached layout results (action thread, recomputed only when viewport/config changes)\n    @Volatile private var layoutDirty: Boolean = true\n    private var cachedLaneCount: Int = 1\n    private var cachedTopFixedLaneCount: Int = 1\n    private var cachedBottomFixedLaneCount: Int = 1\n    private var cachedLaneHeight: Float = 18f\n    private var cachedTextBoxHeight: Float = 18f\n    private var cachedUsableHeight: Int = 0\n    private var cachedTopFixedUsableHeight: Int = 0\n    private var cachedBottomFixedUsableHeight: Int = 0\n    private var cachedTopInset: Int = 0\n    private var cachedMarginPx: Float = 12f\n\n    // Draw paint (main thread)\n    private val bitmapPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { isFilterBitmap = true }\n\n    fun updateViewport(width: Int, height: Int, topInsetPx: Int, bottomInsetPx: Int) {\n        viewportWidth = width.coerceAtLeast(0)\n        viewportHeight = height.coerceAtLeast(0)\n        viewportTopInsetPx = topInsetPx.coerceAtLeast(0)\n        viewportBottomInsetPx = bottomInsetPx.coerceAtLeast(0)\n        layoutDirty = true\n    }\n\n    fun updateConfig(newConfig: DanmakuConfig) {\n        synchronized(actionStateLock) {\n            val old = config\n            config = newConfig\n            val tsPx = sp(newConfig.textSizeSp).coerceAtLeast(1f)\n            val newStroke = newConfig.strokeWidthPx.coerceAtLeast(0).toFloat()\n            val newTypeface = newConfig.fontWeight.typeface\n\n            val styleChanged = textSizePx != tsPx || strokeWidthPx != newStroke || actionPaint.typeface != newTypeface || old.textSizeScale != newConfig.textSizeScale\n            textSizePx = tsPx\n            strokeWidthPx = newStroke\n            outlinePadPx = max(1f, newStroke / 2f)\n            actionPaint.textSize = tsPx\n            if (actionPaint.typeface != newTypeface) actionPaint.typeface = newTypeface\n\n            if (styleChanged) {\n                cacheStyleGeneration++\n                val releaseAt = currentUiFrameId + 1\n                for (a in active) {\n                    val bmp = a.cacheBitmap\n                    if (bmp != null) { cacheManager.enqueueRelease(bmp, releaseAt); a.cacheBitmap = null }\n                    a.cacheState = DanmakuCacheState.Init\n                    a.cacheGeneration = -1\n                }\n            }\n\n            // Rebuild filter if changed\n            val filterChanged = old.allowScroll != newConfig.allowScroll || old.allowTop != newConfig.allowTop ||\n                old.allowBottom != newConfig.allowBottom || old.minLevel != newConfig.minLevel\n            if (filterChanged) {\n                rebuildFilteredItems()\n            }\n            layoutDirty = true\n        }\n    }\n\n    fun stepTime(positionMs: Double, uiFrameId: Int) {\n        currentPositionMs = positionMs.coerceAtLeast(0.0)\n        currentUiFrameId = uiFrameId\n    }\n\n    fun currentPositionMs(): Double = currentPositionMs\n\n    fun drainReleasedBitmaps(uiFrameId: Int) {\n        cacheManager.drainReleasedBitmaps(uiFrameId)\n    }\n\n    fun act() {\n        try {\n            synchronized(actionStateLock) {\n                if (DanmakuLogStats.logEnabled) {\n                    // FPS tracking\n                    actStartNanos = System.nanoTime()\n                    actFrameCount++\n                    if (actLastFrameNanos > 0L) {\n                        val frameDelta = actStartNanos - actLastFrameNanos\n                        if (frameDelta > actFrameDeadlineNanos * 2) actDroppedFrames++\n                    }\n                    actLastFrameNanos = actStartNanos\n                }\n\n                val cfg = config\n                if (!cfg.enabled) { clearActives(); publishEmptySnapshot(); return }\n                val width = viewportWidth; val height = viewportHeight\n                if (width <= 0 || height <= 0) { clearActives(); publishEmptySnapshot(); return }\n\n                val outlinePad = outlinePadPx\n                val rawNowMs = currentPositionMs\n                val nowMs = if (rawNowMs >= lastNowMs) rawNowMs else lastNowMs\n                lastNowMs = nowMs\n\n                // Recompute layout only when viewport or config changed\n                if (layoutDirty) {\n                    val areaFraction = cfg.area.coerceIn(0f, 1f)\n                    cachedTopInset = viewportTopInsetPx.coerceIn(0, height)\n                    val bottomInset = ((1f - areaFraction) * viewportBottomInsetPx).toInt().coerceIn(0, height - cachedTopInset)\n                    val availableHeight = (height - cachedTopInset - bottomInset).coerceAtLeast(0)\n\n                    val scaleFactor = cfg.textSizeScale.coerceIn(25, 200) / 100f\n                    val layoutTextSizePx = textSizePx * scaleFactor\n                    actionPaint.textSize = layoutTextSizePx\n                    actionPaint.getFontMetrics(actionFontMetrics)\n                    cachedTextBoxHeight = (actionFontMetrics.descent - actionFontMetrics.ascent) + outlinePad * 2f\n                    val baseLaneHeight = max(18f, cachedTextBoxHeight * 1.15f)\n                    cachedLaneHeight = max(cachedTextBoxHeight, baseLaneHeight * cfg.laneDensity.laneHeightFactor)\n                    cachedUsableHeight = (availableHeight * areaFraction).toInt().coerceAtLeast(0)\n                    cachedLaneCount = max(1, (cachedUsableHeight / cachedLaneHeight).toInt())\n\n                    // Top fixed: area capped at 0.8\n                    val topFixedAreaFraction = min(areaFraction, 0.8f)\n                    cachedTopFixedUsableHeight = (availableHeight * topFixedAreaFraction).toInt().coerceAtLeast(0)\n                    cachedTopFixedLaneCount = max(1, (cachedTopFixedUsableHeight / cachedLaneHeight).toInt())\n\n                    // Bottom fixed: fixed 20% of screen height, always at bottom\n                    cachedBottomFixedUsableHeight = (height * 0.2f).toInt().coerceAtLeast(0)\n                    cachedBottomFixedLaneCount = max(1, (cachedBottomFixedUsableHeight / cachedLaneHeight).toInt())\n\n                    cachedMarginPx = max(12f, (layoutTextSizePx + outlinePad * 2f) * 0.6f)\n                    layoutDirty = false\n                    snapshotDirty = true\n                }\n                val topInset = cachedTopInset\n                val textBoxHeight = cachedTextBoxHeight\n                val laneHeight = cachedLaneHeight\n                val usableHeight = cachedUsableHeight\n                val laneCount = cachedLaneCount\n                val topFixedLaneCount = cachedTopFixedLaneCount\n                val topFixedUsableHeight = cachedTopFixedUsableHeight\n                val bottomFixedLaneCount = cachedBottomFixedLaneCount\n                val bottomFixedUsableHeight = cachedBottomFixedUsableHeight\n\n                val durationMul = cfg.durationMultiplier.coerceIn(0.2f, 5f)\n                val rollingDurationMs = (DEFAULT_ROLLING_DURATION_MS * durationMul).toInt().coerceIn(MIN_ROLLING_DURATION_MS, MAX_ROLLING_DURATION_MS)\n                val fixedDurationMs = (FIXED_DURATION_MS * durationMul).toInt().coerceIn(MIN_ROLLING_DURATION_MS, MAX_ROLLING_DURATION_MS)\n\n                pruneExpired(width, nowMs)\n                skipOld(nowMs, rollingDurationMs)\n                dropIfLagging(nowMs)\n                ensureLaneStateBuffers(laneCount, topFixedLaneCount, bottomFixedLaneCount)\n                for (lane in 0 until laneCount) cleanupScrollLaneQueue(scrollLaneQueues[lane], width, nowMs)\n\n                val marginPx = cachedMarginPx\n\n                // Spawn new\n                var spawnAttempts = 0\n                while (index < items.size && items[index].timeMs() <= nowMs) {\n                    if (spawnAttempts >= MAX_SPAWN_PER_FRAME) break\n                    val item = items[index]; index++; spawnAttempts++\n                    if (item.data.text.isBlank()) continue\n                    val textWidth = measureTextWidth(item, outlinePad, cfg)\n                    when (item.data.mode) {\n                        Danmaku.MODE_TOP -> trySpawnFixed(DanmakuKind.TOP, item, textWidth, topFixedLaneCount, fixedDurationMs, nowMs)\n                        Danmaku.MODE_BOTTOM -> trySpawnFixed(DanmakuKind.BOTTOM, item, textWidth, bottomFixedLaneCount, fixedDurationMs, nowMs)\n                        else -> trySpawnScroll(item, textWidth, width, laneCount, rollingDurationMs, marginPx, nowMs)\n                    }\n                }\n\n                var style = cachedCacheStyle\n                if (style == null || style.generation != cacheStyleGeneration) {\n                    style = CacheStyle(textSizePx, cfg.textSizeScale, cfg.fontWeight, strokeWidthPx, outlinePad, cacheStyleGeneration)\n                    cachedCacheStyle = style\n                }\n                val releaseAtFrameId = currentUiFrameId + 1\n                requestCacheBuilds(style, releaseAtFrameId)\n                publishSnapshotIfDirty(nowMs, height, topInset, usableHeight, textBoxHeight, laneHeight, topFixedUsableHeight, bottomFixedUsableHeight)\n            }\n        } finally {\n            if (DanmakuLogStats.logEnabled) {\n                val durationNanos = (System.nanoTime() - actStartNanos).coerceAtLeast(0L)\n                recordActDuration(durationNanos)\n\n                val logNow = System.nanoTime()\n                if (logNow - actLastLogNanos >= 1_000_000_000L) {\n                    val elapsed = (logNow - actLastLogNanos) / 1_000_000_000.0\n                    val fps = actFrameCount / elapsed\n                    Log.d(\n                        TAG,\n                        \"[Action] fps=%.1f  frames=%d  dropped=%d  %s  active=%d  cacheQ=%d  mem=%s\".format(\n                            fps,\n                            actFrameCount,\n                            actDroppedFrames,\n                            actionDurationSummary(),\n                            active.size,\n                            cacheManager.queueDepth(),\n                            DanmakuLogStats.memoryUsageSummary(),\n                        )\n                    )\n                    actFrameCount = 0\n                    actDroppedFrames = 0\n                    actLastLogNanos = logNow\n                    resetActionDurationStats()\n                }\n            }\n        }\n    }\n\n    private fun recordActDuration(durationNanos: Long) {\n        actDurationTotalNanos += durationNanos\n        if (durationNanos > actDurationMaxNanos) actDurationMaxNanos = durationNanos\n        if (actDurationSampleCount == actDurationSamples.size) {\n            actDurationSamples = actDurationSamples.copyOf(actDurationSamples.size * 2)\n        }\n        actDurationSamples[actDurationSampleCount++] = durationNanos\n    }\n\n    private fun actionDurationSummary(): String {\n        if (actDurationSampleCount == 0) return \"actMs(avg/p50/p95/max)=0.00/0.00/0.00/0.00\"\n\n        val samples = actDurationSamples.copyOf(actDurationSampleCount)\n        samples.sort()\n        // act 耗时的平均值、中位数、95 分位和最大值。单位是毫秒\n        // 如果 actMs 的 p95/max 明显高于 avg，说明 action 线程存在长尾抖动。\n        val avgMs = actDurationTotalNanos.toDouble() / actDurationSampleCount / 1_000_000.0\n        val p50Ms = percentileNanos(samples, 0.50).toDouble() / 1_000_000.0 // 50% 的样本都不超过这个值，也就是中位数。它表示“典型情况下有多快” \n        val p95Ms = percentileNanos(samples, 0.95).toDouble() / 1_000_000.0 // 95% 的样本都不超过这个值，只有最慢的 5% 会比它更大。它表示“尾部延迟”，也就是偶发慢帧、抖动、卡顿尖峰。\n        val maxMs = actDurationMaxNanos.toDouble() / 1_000_000.0\n        return \"actMs(avg/p50/p95/max)=%.2f/%.2f/%.2f/%.2f\".format(avgMs, p50Ms, p95Ms, maxMs)\n    }\n\n    private fun percentileNanos(sortedSamples: LongArray, percentile: Double): Long {\n        if (sortedSamples.isEmpty()) return 0L\n        val index = ceil((sortedSamples.size - 1) * percentile).toInt().coerceIn(0, sortedSamples.lastIndex)\n        return sortedSamples[index]\n    }\n\n    private fun resetActionDurationStats() {\n        actDurationTotalNanos = 0L\n        actDurationMaxNanos = 0L\n        actDurationSampleCount = 0\n    }\n\n    fun renderSnapshot(): RenderSnapshot =\n        latestSnapshot.also { it.positionMs = currentPositionMs }\n\n    fun draw(canvas: Canvas, snapshot: RenderSnapshot, config: DanmakuConfig) {\n        if (!config.enabled) return\n        val opacityAlpha = (config.opacity * 255f).roundToInt().coerceIn(0, 255)\n        bitmapPaint.alpha = opacityAlpha\n        val styleGen = cacheStyleGeneration\n        val width = viewportWidth\n        val nowMs = currentPositionMs\n\n        for (i in 0 until snapshot.count) {\n            val item = snapshot.items[i] ?: continue\n            val x = when (item.kind) {\n                DanmakuKind.SCROLL -> scrollX(width, nowMs, item.startTimeMs, item.pxPerMs)\n                else -> centerX(width, item.textWidthPx)\n            }\n            val yTop = snapshot.yTop[i]\n            val bmp = item.cacheBitmap\n            if (bmp != null && !bmp.isRecycled && item.cacheGeneration == styleGen) {\n                canvas.drawBitmap(bmp, x, yTop, bitmapPaint)\n            }\n        }\n    }\n\n    // --- Data operations ---\n\n    fun setDanmakus(list: List<Danmaku>) {\n        synchronized(actionStateLock) {\n            clearActives()\n            allItems = list.sortedBy { it.positionMs }.mapTo(ArrayList(list.size)) { DanmakuItem(it) }\n            rebuildFilteredItems()\n            index = 0; lastNowMs = 0.0\n            publishEmptySnapshot()\n        }\n    }\n\n    fun appendDanmakus(list: List<Danmaku>, maxItems: Int, alreadySorted: Boolean) {\n        synchronized(actionStateLock) {\n            if (list.isEmpty()) return\n            if (allItems.isEmpty()) { setDanmakus(list); return }\n            val newItems = if (alreadySorted) list else list.sortedBy { it.positionMs }\n            val lastTime = allItems.lastOrNull()?.timeMs() ?: Int.MIN_VALUE\n            val appendAtEnd = (newItems.firstOrNull()?.positionMs ?: Int.MIN_VALUE) >= lastTime\n            if (appendAtEnd) {\n                for (d in newItems) allItems.add(DanmakuItem(d))\n            } else {\n                for (d in newItems) allItems.add(DanmakuItem(d))\n                allItems.sortBy { it.timeMs() }\n            }\n            if (maxItems > 0) {\n                val drop = allItems.size - maxItems\n                if (drop > 0) allItems = allItems.subList(drop, allItems.size).toMutableList()\n            }\n            rebuildFilteredItems()\n            if (appendAtEnd) {\n                // New items are all after existing ones — keep actives, lane state and index intact.\n                index = index.coerceIn(0, items.size)\n            } else {\n                // Items inserted in middle — recalculate index but keep actives intact.\n                val currentPos = lastNowMs.coerceAtLeast(0.0)\n                index = lowerBound(currentPos)\n            }\n        }\n    }\n\n    fun trimToTimeRange(minTimeMs: Long, maxTimeMs: Long) {\n        synchronized(actionStateLock) {\n            if (allItems.isEmpty()) return\n            val minI = minTimeMs.coerceAtLeast(0L).coerceAtMost(Int.MAX_VALUE.toLong()).toInt()\n            val maxI = maxTimeMs.coerceAtLeast(0L).coerceAtMost(Int.MAX_VALUE.toLong()).toInt()\n            if (maxI <= minI) return\n            allItems.removeAll { it.timeMs() < minI || it.timeMs() >= maxI }\n            rebuildFilteredItems()\n            index = (index).coerceIn(0, items.size)\n        }\n    }\n\n    fun seekTo(positionMs: Double = 0.0) {\n        synchronized(actionStateLock) {\n            index = lowerBound(positionMs)\n            clearActives(); lastNowMs = positionMs\n            publishEmptySnapshot()\n        }\n    }\n\n    fun clear() {\n        synchronized(actionStateLock) { clearActives(); publishEmptySnapshot() }\n    }\n\n    fun release() {\n        synchronized(actionStateLock) {\n            clear()\n            snapshotA.clear()\n            snapshotB.clear()\n            // Recycle cacheBitmaps held by all items (not just active ones)\n            for (item in allItems) {\n                val bmp = item.cacheBitmap\n                if (bmp != null && !bmp.isRecycled) try { bmp.recycle() } catch (_: Exception) {}\n                item.cacheBitmap = null\n            }\n            allItems = mutableListOf()\n            items = mutableListOf()\n        }\n    }\n\n    // --- Private helpers ---\n\n    private fun rebuildFilteredItems() {\n        val cfg = config\n        items = allItems.filterTo(ArrayList(allItems.size)) { item ->\n            val d = item.data\n            if (d.level < cfg.minLevel) return@filterTo false\n            when (d.mode) {\n                Danmaku.MODE_SCROLL -> cfg.allowScroll\n                Danmaku.MODE_TOP -> cfg.allowTop\n                Danmaku.MODE_BOTTOM -> cfg.allowBottom\n                else -> cfg.allowScroll\n            }\n        }\n    }\n\n    private fun trySpawnScroll(item: DanmakuItem, textWidth: Float, width: Int, laneCount: Int, rollingDurationMs: Int, marginPx: Float, nowMs: Double): Boolean {\n        if (item.data.text.isBlank()) return true\n        val widthF = width.toFloat()\n        val durationF = rollingDurationMs.toFloat()\n        val distancePx = (widthF + textWidth).coerceAtLeast(0f)\n        val rawPx = distancePx / durationF\n        val shortPx = widthF / durationF\n        val maxPx = shortPx * MAX_LONG_SCROLL_SPEED_RATIO\n        val pxNew = min(rawPx, maxPx)\n        val durationMs = computeScrollDurationMs(distancePx, pxNew, rollingDurationMs)\n        for (lane in 0 until laneCount) {\n            val queue = scrollLaneQueues[lane]\n            val prev = queue.lastOrNull()\n            if (prev == null) {\n                activate(item, DanmakuKind.SCROLL, lane, textWidth, pxNew, durationMs, nowMs)\n                queue.addLast(item)\n                return true\n            }\n            val tailPrev = scrollX(width, nowMs, prev.startTimeMs, prev.pxPerMs) + prev.textWidthPx\n            if (isScrollLaneAvailable(widthF, nowMs, prev, tailPrev, pxNew, marginPx)) {\n                activate(item, DanmakuKind.SCROLL, lane, textWidth, pxNew, durationMs, nowMs)\n                queue.addLast(item)\n                return true\n            }\n        }\n        return false\n    }\n\n    private fun trySpawnFixed(kind: DanmakuKind, item: DanmakuItem, textWidth: Float, laneCount: Int, fixedDurationMs: Int, nowMs: Double): Boolean {\n        if (item.data.text.isBlank()) return true\n        val busyUntil = when (kind) {\n            DanmakuKind.TOP -> topLaneBusyUntilMs\n            DanmakuKind.BOTTOM -> bottomLaneBusyUntilMs\n            else -> return false\n        }\n        for (lane in 0 until laneCount) {\n            if (busyUntil[lane] <= nowMs) {\n                activate(item, kind, lane, textWidth, 0f, fixedDurationMs, nowMs)\n                busyUntil[lane] = nowMs + fixedDurationMs\n                return true\n            }\n        }\n        return false\n    }\n\n    private fun activate(item: DanmakuItem, kind: DanmakuKind, lane: Int, textWidth: Float, pxPerMs: Float, durationMs: Int, startTimeMs: Double) {\n        item.isActive = true\n        item.kind = kind; item.lane = lane; item.textWidthPx = textWidth\n        item.pxPerMs = pxPerMs; item.durationMs = durationMs; item.startTimeMs = startTimeMs.toInt()\n        active.add(item)\n        snapshotDirty = true\n    }\n\n    private fun clearActives() {\n        resetLaneState()\n        cacheProbeCursor = 0\n        snapshotDirty = true\n        if (active.isEmpty()) return\n        val releaseAt = currentUiFrameId + 1\n        for (i in active.size - 1 downTo 0) releaseItemCache(active.removeAt(i), releaseAt)\n    }\n\n    private fun releaseItemCache(item: DanmakuItem, releaseAtFrameId: Int) {\n        item.isActive = false\n        val bmp = item.cacheBitmap\n        if (bmp != null) { cacheManager.enqueueRelease(bmp, releaseAtFrameId); item.cacheBitmap = null }\n        item.cacheState = DanmakuCacheState.Init; item.cacheGeneration = -1\n    }\n\n    private fun pruneExpired(width: Int, nowMs: Double) {\n        if (active.isEmpty()) return\n        val releaseAt = currentUiFrameId + 1\n        var write = 0\n        for (read in 0 until active.size) {\n            val a = active[read]\n            val keep = !isExpired(width, nowMs, a)\n            if (!keep) {\n                releaseItemCache(a, releaseAt)\n                snapshotDirty = true\n                continue\n            }\n            if (write != read) active[write] = a\n            write++\n        }\n        if (write < active.size) active.subList(write, active.size).clear()\n        if (cacheProbeCursor >= active.size) cacheProbeCursor = 0\n    }\n\n    private fun skipOld(nowMs: Double, rollingDurationMs: Int) {\n        val ignoreBefore = nowMs - rollingDurationMs\n        while (index < items.size && items[index].timeMs() < ignoreBefore) index++\n    }\n\n    private fun dropIfLagging(nowMs: Double) {\n        val dropBefore = nowMs - MAX_CATCH_UP_LAG_MS\n        while (index < items.size && items[index].timeMs() < dropBefore) index++\n    }\n\n    private fun publishEmptySnapshot() {\n        val out = writableSnapshot()\n        out.clear()\n        latestSnapshot = out\n        snapshotDirty = false\n    }\n    private fun writableSnapshot(): RenderSnapshot = if (latestSnapshot === snapshotA) snapshotB else snapshotA\n\n    private fun scrollX(width: Int, nowMs: Double, startTimeMs: Int, pxPerMs: Float): Float =\n        (width.toFloat() - (nowMs - startTimeMs).coerceAtLeast(0.0) * pxPerMs).toFloat()\n\n    private fun centerX(width: Int, contentWidth: Float): Float =\n        if (width <= 0) 0f else ((width.toFloat() - contentWidth) / 2f).coerceAtLeast(0f)\n\n    private fun isScrollLaneAvailable(width: Float, nowMs: Double, front: DanmakuItem, tailPrev: Float, pxNew: Float, marginPx: Float): Boolean {\n        val elapsedPrev = nowMs - front.startTimeMs\n        val prevRemaining = front.durationMs - elapsedPrev\n        if (prevRemaining <= 0) return true\n        if (tailPrev + marginPx > width) return false\n        val pxPrev = front.pxPerMs\n        if (pxNew <= pxPrev) return true\n        val gap0 = (width - tailPrev - marginPx).coerceAtLeast(0f)\n        return gap0 >= (pxNew - pxPrev) * prevRemaining\n    }\n\n    private fun computeScrollDurationMs(distancePx: Float, pxPerMs: Float, fallback: Int): Int {\n        if (!distancePx.isFinite() || distancePx <= 0f || !pxPerMs.isFinite() || pxPerMs <= 0f) return fallback.coerceAtLeast(1)\n        return max(fallback.coerceAtLeast(1), ceil(distancePx / pxPerMs).toInt().coerceAtLeast(1))\n    }\n\n\n    private fun measureTextWidth(item: DanmakuItem, outlinePad: Float, cfg: DanmakuConfig): Float {\n        val text = item.data.text\n        if (text.isBlank()) return outlinePad * 2f\n        // Compute effective text size for measurement\n        val clampedSize = min(item.data.textSize, 25)\n        val scaleFactor = cfg.textSizeScale.coerceIn(25, 200) / 100f\n        val effectiveTextSizePx = (textSizePx * clampedSize / 25f * scaleFactor).coerceAtLeast(1f)\n        actionPaint.textSize = effectiveTextSizePx\n        return actionPaint.measureText(text) + outlinePad * 2f\n    }\n\n    private fun lowerBound(pos: Double): Int {\n        var l = 0; var r = items.size\n        while (l < r) { val m = (l + r) ushr 1; if (items[m].timeMs() < pos) l = m + 1 else r = m }\n        return l\n    }\n\n    private fun requestCacheBuilds(style: CacheStyle, releaseAtFrameId: Int) {\n        if (active.isEmpty()) return\n        if (cacheManager.queueDepth() >= MAX_CACHE_QUEUE_DEPTH) return\n        val scanCount = min(active.size, MAX_CACHE_SCAN_PER_FRAME)\n        var requested = 0\n        for (offset in 0 until scanCount) {\n            if (requested >= MAX_CACHE_REQUESTS_PER_FRAME) break\n            val indexInActive = (cacheProbeCursor + offset) % active.size\n            val item = active[indexInActive]\n            val bmp = item.cacheBitmap\n            val hasValidCache = bmp != null && !bmp.isRecycled && item.cacheGeneration == style.generation\n            if (hasValidCache) continue\n            if (item.cacheState == DanmakuCacheState.Rendering) continue\n            item.cacheState = DanmakuCacheState.Rendering\n            cacheManager.requestBuildCache(item, item.textWidthPx, style, releaseAtFrameId)\n            requested++\n        }\n        cacheProbeCursor = if (active.isEmpty()) 0 else (cacheProbeCursor + scanCount) % active.size\n    }\n\n    private fun publishSnapshotIfDirty(\n        nowMs: Double,\n        height: Int,\n        topInset: Int,\n        usableHeight: Int,\n        textBoxHeight: Float,\n        laneHeight: Float,\n        topFixedUsableHeight: Int,\n        bottomFixedUsableHeight: Int,\n    ) {\n        if (!snapshotDirty) return\n        val topInsetF = topInset.toFloat()\n        val maxYTop = (topInset + usableHeight - textBoxHeight).coerceAtLeast(topInsetF)\n        val topFixedMaxYTop = (topInset + topFixedUsableHeight - textBoxHeight).coerceAtLeast(topInsetF)\n        val bottomFixedBaseYTop = height - viewportBottomInsetPx - textBoxHeight\n        val bottomFixedMinYTop = (height - viewportBottomInsetPx - bottomFixedUsableHeight).toFloat().coerceAtLeast(0f)\n        val out = writableSnapshot()\n        out.ensureCapacity(active.size)\n        out.positionMs = nowMs\n        out.count = 0\n        for (a in active) {\n            val iOut = out.count\n            val yTop = when (a.kind) {\n                DanmakuKind.SCROLL -> (topInsetF + laneHeight * a.lane).coerceAtMost(maxYTop)\n                DanmakuKind.TOP -> (topInsetF + laneHeight * a.lane).coerceAtMost(topFixedMaxYTop)\n                DanmakuKind.BOTTOM -> (bottomFixedBaseYTop - laneHeight * a.lane).coerceAtLeast(bottomFixedMinYTop)\n            }\n            out.items[iOut] = a\n            out.yTop[iOut] = yTop\n            out.count = iOut + 1\n        }\n        latestSnapshot = out\n        snapshotDirty = false\n    }\n\n    private fun ensureLaneStateBuffers(scrollLaneCount: Int, topLaneCount: Int, bottomLaneCount: Int) {\n        if (scrollLaneQueues.size < scrollLaneCount) {\n            val old = scrollLaneQueues\n            scrollLaneQueues = Array(scrollLaneCount) { idx -> old.getOrNull(idx) ?: ArrayDeque() }\n        }\n        if (topLaneBusyUntilMs.size < topLaneCount) topLaneBusyUntilMs = topLaneBusyUntilMs.copyOf(topLaneCount)\n        if (bottomLaneBusyUntilMs.size < bottomLaneCount) bottomLaneBusyUntilMs = bottomLaneBusyUntilMs.copyOf(bottomLaneCount)\n    }\n\n    private fun resetLaneState() {\n        for (queue in scrollLaneQueues) queue.clear()\n        topLaneBusyUntilMs.fill(0.0)\n        bottomLaneBusyUntilMs.fill(0.0)\n    }\n\n    private fun cleanupScrollLaneQueue(queue: ArrayDeque<DanmakuItem>, width: Int, nowMs: Double) {\n        while (queue.isNotEmpty()) { val f = queue.first(); if (!f.isActive || isExpired(width, nowMs, f)) queue.removeFirst() else break }\n        while (queue.isNotEmpty()) { val l = queue.last(); if (!l.isActive || isExpired(width, nowMs, l)) queue.removeLast() else break }\n    }\n\n    private fun isExpired(width: Int, nowMs: Double, item: DanmakuItem): Boolean {\n        val elapsed = nowMs - item.startTimeMs\n        if (elapsed >= item.durationMs) return true\n        return item.kind == DanmakuKind.SCROLL && scrollX(width, nowMs, item.startTimeMs, item.pxPerMs) + item.textWidthPx < 0f\n    }\n\n    private fun sp(v: Float): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, v, displayMetrics)\n\n    private companion object {\n        private const val TAG = \"DanmakuEngine\"\n        // 滚动弹幕的基础穿屏时长（durationMultiplier = 1.0 时）。\n        const val DEFAULT_ROLLING_DURATION_MS = 7_800f\n        // 弹幕显示时长下限，避免过快难以阅读。\n        const val MIN_ROLLING_DURATION_MS = 2_500\n        // 弹幕显示时长上限，避免长时间占轨。\n        const val MAX_ROLLING_DURATION_MS = 20_000\n        // 固定弹幕的基础停留时长（durationMultiplier = 1.0 时）。\n        const val FIXED_DURATION_MS = 4_000\n        // 长弹幕允许比短弹幕更快，但最多只放大到短弹幕基准速度的这个倍数。\n        const val MAX_LONG_SCROLL_SPEED_RATIO = 1.5f\n        // 单帧最多尝试生成多少条到时弹幕，防止瞬时高峰拖垮 action 线程。\n        const val MAX_SPAWN_PER_FRAME = 48\n        // 单帧最多探测多少个 active 项来补缓存，避免每帧全量扫描 active。\n        const val MAX_CACHE_SCAN_PER_FRAME = 16\n        // 播放时间落后太多时，直接跳过更早的弹幕，优先追上当前播放进度。\n        const val MAX_CATCH_UP_LAG_MS = 1_200\n        // 单帧最多向缓存线程提交多少个位图构建请求，限制异步渲染压力。\n        const val MAX_CACHE_REQUESTS_PER_FRAME = 12\n        // 缓存构建队列的最大深度，超过后本帧不再继续提交新任务。\n        const val MAX_CACHE_QUEUE_DEPTH = 64\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/DanmakuLogStats.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku\n\nimport android.os.Debug\nimport java.util.Locale\n\ninternal object DanmakuLogStats {\n    @Volatile\n    var logEnabled: Boolean = false\n\n    private const val BYTES_PER_MB = 1024f * 1024f\n\n    fun memoryUsageSummary(): String {\n        val runtime = Runtime.getRuntime()\n        val heapUsedBytes = (runtime.totalMemory() - runtime.freeMemory()).coerceAtLeast(0L)\n        val heapMaxBytes = runtime.maxMemory().coerceAtLeast(0L)\n        val nativeUsedBytes = Debug.getNativeHeapAllocatedSize().coerceAtLeast(0L)\n        return String.format(\n            Locale.US,\n            \"heap=%.2f/%.2fMB native=%.2fMB\",\n            heapUsedBytes / BYTES_PER_MB,\n            heapMaxBytes / BYTES_PER_MB,\n            nativeUsedBytes / BYTES_PER_MB,\n        )\n    }\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/DanmakuPlayer.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku\n\nimport android.graphics.Canvas\nimport android.os.Handler\nimport android.os.HandlerThread\nimport android.os.Looper\nimport android.os.Message\nimport android.util.Log\nimport android.view.Choreographer\nimport dev.aaa1115910.bv.player.danmaku.model.Danmaku\nimport dev.aaa1115910.bv.player.danmaku.model.RenderSnapshot\nimport java.util.concurrent.Semaphore\nimport java.util.concurrent.atomic.AtomicInteger\n\ninternal class DanmakuPlayer(private val view: DanmakuView) {\n\n    private val cacheManager = CacheManager(\n        mainLooper = Looper.getMainLooper(),\n        onRenderSign = { view.postInvalidateOnAnimation() },\n    )\n    private val engine = DanmakuEngine(view.resources.displayMetrics, cacheManager)\n    private val timer = DanmakuTimer()\n    private val drawSemaphore = Semaphore(0)\n    private val actionThread = HandlerThread(\"Danmaku-Action\").apply { start() }\n    private val actionHandler = ActionHandler(actionThread.looper)\n    private val frameCallback = FrameCallback(actionHandler)\n    private val seekSerial = AtomicInteger(0)\n    private val uiFrameId = AtomicInteger(0)\n\n    // Draw FPS stats (main thread)\n    private var drawFrameCount: Int = 0\n    private var drawDroppedFrames: Int = 0\n    private var drawLastLogNanos: Long = System.nanoTime()\n    private var drawLastFrameNanos: Long = 0L\n    private val drawFrameDeadlineNanos: Long = 16_666_667L // ~60fps\n    private var drawRepeatedSnapshotCount: Int = 0\n    private var drawCurrentRepeatedSnapshotStreak: Int = 0\n    private var drawMaxRepeatedSnapshotStreak: Int = 0\n    private var drawSnapshotAgeTotalMs: Double = 0.0\n    private var drawSnapshotAgeMaxMs: Double = 0.0\n    private var drawLastSnapshotPositionMs: Double = Double.NEGATIVE_INFINITY\n\n    @Volatile private var started: Boolean = false\n    @Volatile private var released: Boolean = false\n    @Volatile private var viewportWidth: Int = 0\n    @Volatile private var viewportHeight: Int = 0\n    @Volatile private var viewportTopInsetPx: Int = 0\n    @Volatile private var viewportBottomInsetPx: Int = 0\n    @Volatile private var latestConfig: DanmakuConfig? = null\n    private var lastEnabled: Boolean = true\n\n    fun startIfNeeded() {\n        if (released || started) return\n        started = true\n        actionHandler.post { postFrameCallback() }\n        view.postInvalidateOnAnimation()\n    }\n\n    fun stop() {\n        if (!started) return\n        started = false\n        releaseSemaphoreIfNeeded()\n        Choreographer.getInstance().removeFrameCallback(frameCallback)\n    }\n\n    fun release() {\n        if (released) return\n        released = true; started = false\n        releaseSemaphoreIfNeeded()\n        try { actionHandler.obtainMessage(MSG_OP_RELEASE).sendToTarget() } catch (_: Exception) {}\n    }\n\n    fun onViewportChanged(width: Int, height: Int, topInsetPx: Int, bottomInsetPx: Int) {\n        viewportWidth = width.coerceAtLeast(0)\n        viewportHeight = height.coerceAtLeast(0)\n        viewportTopInsetPx = topInsetPx.coerceAtLeast(0)\n        viewportBottomInsetPx = bottomInsetPx.coerceAtLeast(0)\n        actionHandler.removeMessages(MSG_OP_VIEWPORT)\n        actionHandler.sendEmptyMessage(MSG_OP_VIEWPORT)\n    }\n\n    fun updateConfig(config: DanmakuConfig) {\n        latestConfig = config\n        actionHandler.removeMessages(MSG_OP_CONFIG)\n        actionHandler.sendEmptyMessage(MSG_OP_CONFIG)\n    }\n\n    fun setDanmakus(list: List<Danmaku>) {\n        actionHandler.obtainMessage(MSG_OP_SET, list).sendToTarget()\n    }\n\n    fun appendDanmakus(list: List<Danmaku>, maxItems: Int, alreadySorted: Boolean) {\n        actionHandler.obtainMessage(MSG_OP_APPEND, AppendPayload(list, maxItems, alreadySorted)).sendToTarget()\n    }\n\n    fun trimToTimeRange(minTimeMs: Long, maxTimeMs: Long) {\n        actionHandler.obtainMessage(MSG_OP_TRIM_RANGE, TrimRangePayload(minTimeMs, maxTimeMs)).sendToTarget()\n    }\n\n    fun seekTo(positionMs: Long) {\n        seekSerial.incrementAndGet()\n        actionHandler.obtainMessage(MSG_OP_SEEK, positionMs.toDouble()).sendToTarget()\n    }\n\n    fun draw(canvas: Canvas, rawPositionMs: Long, isPlaying: Boolean, playbackSpeed: Float, config: DanmakuConfig) {\n        if (released) return\n        if (!config.enabled) {\n            if (lastEnabled || started) stop()\n            if (lastEnabled) requestClear()\n            lastEnabled = false; return\n        }\n        lastEnabled = true\n        if (isPlaying) startIfNeeded()\n        else if (started) { started = false; releaseSemaphoreIfNeeded(); Choreographer.getInstance().removeFrameCallback(frameCallback) }\n\n        val frameId = uiFrameId.incrementAndGet()\n        engine.drainReleasedBitmaps(frameId)\n        val smoothPos = timer.step(System.nanoTime(), rawPositionMs, isPlaying, playbackSpeed, seekSerial.get())\n        engine.stepTime(smoothPos, frameId)\n        drawSemaphore.tryAcquire()\n        val snapshot = engine.renderSnapshot()\n        releaseSemaphoreIfNeeded()\n\n        // 日志统计：FPS、丢帧、画重复快照的次数、快照过时程度、内存使用等。开启后可以通过 logcat 观察这些指标的变化，帮助分析性能瓶颈和优化效果。\n        if (DanmakuLogStats.logEnabled) {\n            val drawNow = System.nanoTime()\n            drawFrameCount++\n            if (drawLastFrameNanos > 0L) {\n                val delta = drawNow - drawLastFrameNanos\n                if (delta > drawFrameDeadlineNanos * 2) drawDroppedFrames++\n            }\n            drawLastFrameNanos = drawNow\n            sampleDrawSnapshotStats(snapshot.positionMs, smoothPos)\n            if (drawNow - drawLastLogNanos >= 1_000_000_000L) {\n                val elapsed = (drawNow - drawLastLogNanos) / 1_000_000_000.0\n                val fps = drawFrameCount / elapsed\n                val avgSnapshotAgeMs = if (drawFrameCount > 0) drawSnapshotAgeTotalMs / drawFrameCount else 0.0\n\n                // 如果 sameSnap 基本是 0，说明现有 acquire 模式几乎没有重复旧快照。\n                Log.d(\n                    TAG,\n                    \"[Draw] fps=%.1f  frames=%d  dropped=%d  sameSnap=%d  maxSameSnapStreak=%d  snapAgeMs(avg/max)=%.1f/%.1f  mem=%s\".format(\n                        fps,\n                        drawFrameCount,\n                        drawDroppedFrames,\n                        drawRepeatedSnapshotCount, // 这一秒里重复画到同一个 snapshot 的次数\n                        drawMaxRepeatedSnapshotStreak, // 连续重复同一个 snapshot 的最长次数\n                        avgSnapshotAgeMs, // 平均每一帧画的 snapshot 和当前时间的差距，单位毫秒。这个值越大说明越多帧在画过时的弹幕，可能会有明显的卡顿感。\n                        drawSnapshotAgeMaxMs, // 这一秒里画过的 snapshot 中，最过时的那个和当前时间的差距，单位毫秒。这个值越大说明偶尔会有非常过时的弹幕被画出来，可能会有明显的卡顿尖峰。\n                        DanmakuLogStats.memoryUsageSummary(),\n                    )\n                )\n                drawFrameCount = 0\n                drawDroppedFrames = 0\n                drawLastLogNanos = drawNow\n                resetDrawSnapshotStats()\n            }\n        }\n        engine.draw(canvas, snapshot, config)\n    }\n\n    private fun sampleDrawSnapshotStats(snapshotPositionMs: Double, smoothPos: Double) {\n        if (snapshotPositionMs == drawLastSnapshotPositionMs) {\n            drawRepeatedSnapshotCount++\n            drawCurrentRepeatedSnapshotStreak++\n            if (drawCurrentRepeatedSnapshotStreak > drawMaxRepeatedSnapshotStreak) {\n                drawMaxRepeatedSnapshotStreak = drawCurrentRepeatedSnapshotStreak\n            }\n        } else {\n            drawCurrentRepeatedSnapshotStreak = 0\n            drawLastSnapshotPositionMs = snapshotPositionMs\n        }\n\n        val snapshotAgeMs = (smoothPos - snapshotPositionMs).coerceAtLeast(0.0)\n        drawSnapshotAgeTotalMs += snapshotAgeMs\n        if (snapshotAgeMs > drawSnapshotAgeMaxMs) drawSnapshotAgeMaxMs = snapshotAgeMs\n    }\n\n    private fun resetDrawSnapshotStats() {\n        drawRepeatedSnapshotCount = 0\n        drawCurrentRepeatedSnapshotStreak = 0\n        drawMaxRepeatedSnapshotStreak = 0\n        drawSnapshotAgeTotalMs = 0.0\n        drawSnapshotAgeMaxMs = 0.0\n        drawLastSnapshotPositionMs = Double.NEGATIVE_INFINITY\n    }\n\n    private fun postFrameCallback() {\n        if (released || !started) return\n        Choreographer.getInstance().postFrameCallback(frameCallback)\n    }\n\n    private fun requestClear() {\n        if (released) return\n        actionHandler.removeMessages(MSG_OP_CLEAR)\n        actionHandler.sendEmptyMessage(MSG_OP_CLEAR)\n    }\n\n    private fun releaseSemaphoreIfNeeded() {\n        if (drawSemaphore.availablePermits() == 0) drawSemaphore.release()\n    }\n\n    private inner class ActionHandler(looper: Looper) : Handler(looper) {\n        override fun handleMessage(msg: Message) {\n            when (msg.what) {\n                MSG_FRAME_UPDATE -> {\n                    if (released || !started) return\n                    postFrameCallback()\n                    try {\n                        drawSemaphore.acquire()\n                        if (released || !started) return\n                        engine.act()\n                        view.postInvalidateOnAnimation()\n                    } catch (_: InterruptedException) {}\n                }\n                MSG_OP_SET -> {\n                    @Suppress(\"UNCHECKED_CAST\")\n                    engine.setDanmakus(msg.obj as? List<Danmaku> ?: emptyList())\n                    renderOnceIfPaused()\n                }\n                MSG_OP_APPEND -> {\n                    val p = msg.obj as? AppendPayload ?: return\n                    engine.appendDanmakus(p.list, p.maxItems, p.alreadySorted)\n                    renderOnceIfPaused()\n                }\n                MSG_OP_TRIM_RANGE -> {\n                    val p = msg.obj as? TrimRangePayload ?: return\n                    engine.trimToTimeRange(p.minTimeMs, p.maxTimeMs)\n                    renderOnceIfPaused()\n                }\n                MSG_OP_SEEK -> {\n                    val pos = (msg.obj as? Double) ?: 0.0\n                    engine.seekTo(pos)\n                    renderOnceIfPaused(pos)\n                }\n                MSG_OP_CLEAR -> engine.clear()\n                MSG_OP_VIEWPORT -> {\n                    engine.updateViewport(viewportWidth, viewportHeight, viewportTopInsetPx, viewportBottomInsetPx)\n                    renderOnceIfPaused()\n                }\n                MSG_OP_CONFIG -> {\n                    latestConfig?.let { newCfg ->\n                        val oldCfg = engine.config\n                        engine.updateConfig(newCfg)\n                        // Only seekTo (clear + re-spawn) if layout-affecting properties changed.\n                        // Opacity is appearance-only and should not cause a full reset.\n                        val layoutChanged = oldCfg.enabled != newCfg.enabled ||\n                            oldCfg.textSizeSp != newCfg.textSizeSp ||\n                            oldCfg.textSizeScale != newCfg.textSizeScale ||\n                            oldCfg.fontWeight != newCfg.fontWeight ||\n                            oldCfg.strokeWidthPx != newCfg.strokeWidthPx ||\n                            oldCfg.durationMultiplier != newCfg.durationMultiplier ||\n                            oldCfg.area != newCfg.area ||\n                            oldCfg.laneDensity != newCfg.laneDensity ||\n                            oldCfg.allowScroll != newCfg.allowScroll ||\n                            oldCfg.allowTop != newCfg.allowTop ||\n                            oldCfg.allowBottom != newCfg.allowBottom ||\n                            oldCfg.minLevel != newCfg.minLevel\n                        if (layoutChanged) {\n                            engine.seekTo(engine.currentPositionMs())\n                        }\n                        renderOnceIfPaused()\n                    }\n                }\n                MSG_OP_RELEASE -> {\n                    removeCallbacksAndMessages(null)\n                    Choreographer.getInstance().removeFrameCallback(frameCallback)\n                    started = false\n                    try { actionThread.quitSafely() } catch (_: Exception) {}\n                    engine.release()\n                    cacheManager.release()\n                }\n            }\n        }\n\n        private fun renderOnceIfPaused(positionMs: Double? = null) {\n            if (released || started) return\n            val pos = positionMs ?: engine.currentPositionMs()\n            engine.stepTime(pos, uiFrameId.get())\n            try { engine.act() } catch (_: Exception) {}\n            view.postInvalidateOnAnimation()\n        }\n    }\n\n    private class FrameCallback(private val handler: Handler) : Choreographer.FrameCallback {\n        override fun doFrame(frameTimeNanos: Long) {\n            handler.removeMessages(MSG_FRAME_UPDATE)\n            handler.sendEmptyMessage(MSG_FRAME_UPDATE)\n        }\n    }\n\n    private class AppendPayload(val list: List<Danmaku>, val maxItems: Int, val alreadySorted: Boolean)\n    private class TrimRangePayload(val minTimeMs: Long, val maxTimeMs: Long)\n\n    companion object {\n        private const val TAG = \"DanmakuPlayer\"\n        private const val MSG_FRAME_UPDATE = 2101\n        private const val MSG_OP_SET = 3101\n        private const val MSG_OP_APPEND = 3102\n        private const val MSG_OP_TRIM_RANGE = 3103\n        private const val MSG_OP_SEEK = 3105\n        private const val MSG_OP_CLEAR = 3106\n        private const val MSG_OP_VIEWPORT = 3201\n        private const val MSG_OP_CONFIG = 3202\n        private const val MSG_OP_RELEASE = 3999\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/DanmakuTimer.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku\n\nimport kotlin.math.abs\n\ninternal class DanmakuTimer {\n    private var lastFrameNanos: Long = 0L\n    private var smoothPositionMs: Double = 0.0\n    private var lastSeekSerial: Int = 0\n    private var lastPlaying: Boolean = false\n    private var lastPlaybackSpeed: Double = 1.0\n\n    fun reset(positionMs: Long, nowNanos: Long, seekSerial: Int, isPlaying: Boolean, playbackSpeed: Float) {\n        lastFrameNanos = nowNanos\n        smoothPositionMs = positionMs.coerceAtLeast(0L).toDouble()\n        lastSeekSerial = seekSerial\n        lastPlaying = isPlaying\n        lastPlaybackSpeed = normalizeSpeed(playbackSpeed)\n    }\n\n    fun step(nowNanos: Long, rawPositionMs: Long, isPlaying: Boolean, playbackSpeed: Float, seekSerial: Int): Double {\n        val raw = rawPositionMs.coerceAtLeast(0L).toDouble()\n        val speed = normalizeSpeed(playbackSpeed)\n\n        if (lastFrameNanos == 0L || seekSerial != lastSeekSerial) {\n            reset(rawPositionMs, nowNanos, seekSerial, isPlaying, playbackSpeed)\n            return smoothPositionMs\n        }\n\n        val dtNanos = (nowNanos - lastFrameNanos).coerceAtLeast(0L)\n        lastFrameNanos = nowNanos\n        lastSeekSerial = seekSerial\n\n        if (!isPlaying) {\n            if (lastPlaying || abs(raw - smoothPositionMs) >= IDLE_REANCHOR_THRESHOLD_MS) {\n                smoothPositionMs = raw\n            }\n            lastPlaying = false\n            lastPlaybackSpeed = speed\n            return smoothPositionMs\n        }\n\n        if (!lastPlaying || abs(speed - lastPlaybackSpeed) >= SPEED_CHANGE_EPSILON) {\n            smoothPositionMs = raw\n            lastPlaying = true\n            lastPlaybackSpeed = speed\n            return smoothPositionMs\n        }\n\n        if (dtNanos > 0L) {\n            smoothPositionMs += dtNanos.toDouble() / 1_000_000.0 * speed\n        }\n\n        if (!smoothPositionMs.isFinite() || abs(smoothPositionMs) > 1e15) smoothPositionMs = raw\n        if (smoothPositionMs < 0.0) smoothPositionMs = 0.0\n        if (abs(raw - smoothPositionMs) >= EXTREME_DRIFT_REANCHOR_THRESHOLD_MS) smoothPositionMs = raw\n\n        lastPlaying = true\n        lastPlaybackSpeed = speed\n        return smoothPositionMs\n    }\n\n    private fun normalizeSpeed(playbackSpeed: Float): Double =\n        if (playbackSpeed.isFinite() && playbackSpeed > 0f) playbackSpeed.toDouble() else 1.0\n\n    private companion object {\n        const val IDLE_REANCHOR_THRESHOLD_MS = 120.0\n        const val EXTREME_DRIFT_REANCHOR_THRESHOLD_MS = 1_000.0\n        const val SPEED_CHANGE_EPSILON = 0.0001\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/DanmakuView.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.Paint\nimport android.graphics.PorterDuff\nimport android.graphics.PorterDuffXfermode\nimport android.graphics.Rect\nimport android.os.SystemClock\nimport android.util.AttributeSet\nimport android.util.TypedValue\nimport android.view.View\nimport com.caverock.androidsvg.SVG\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskFrame\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMobMaskFrame\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuWebMaskFrame\nimport dev.aaa1115910.bv.player.entity.VideoAspectRatio\nimport dev.aaa1115910.bv.player.danmaku.model.Danmaku\n\nclass DanmakuView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : View(context, attrs) {\n\n    private val player = DanmakuPlayer(this)\n    private var currentLayerType: Int = LAYER_TYPE_NONE\n\n    private var positionProvider: (() -> Long)? = null\n    private var isPlayingProvider: (() -> Boolean)? = null\n    private var playbackSpeedProvider: (() -> Float)? = null\n    private var config: DanmakuConfig = DEFAULT_CONFIG\n    private var lastRawPositionMs: Long = 0L\n    private var lastPositionChangeUptimeMs: Long = 0L\n\n    private val viewportTopInsetPx: Int = dp(2f)\n    private val viewportBottomInsetPx: Int = dp(2f)\n    private var lastViewportW: Int = 0\n    private var lastViewportH: Int = 0\n    private var lastViewportTopInset: Int = 0\n    private var lastViewportBottomInset: Int = 0\n\n    @Volatile private var maskFrame: DanmakuMaskFrame? = null\n    private var cachedMaskFrame: DanmakuMaskFrame? = null\n    private var cachedMaskBitmap: Bitmap? = null\n    @Volatile private var videoAspectRatio: Float = 0f\n    @Volatile private var videoAspectRatioType: VideoAspectRatio = VideoAspectRatio.Default\n\n    private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply {\n        xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)\n    }\n    private val maskDstRect = Rect()\n\n    fun setPositionProvider(provider: () -> Long) { positionProvider = provider }\n    fun setIsPlayingProvider(provider: () -> Boolean) { isPlayingProvider = provider }\n    fun setPlaybackSpeedProvider(provider: () -> Float) { playbackSpeedProvider = provider }\n    fun setConfig(config: DanmakuConfig) {\n        if (this.config == config) return\n        this.config = config\n        player.updateConfig(config)\n        postInvalidateOnAnimation()\n    }\n\n    fun setDanmakus(list: List<Danmaku>) { player.setDanmakus(list); invalidate() }\n    fun appendDanmakus(list: List<Danmaku>, maxItems: Int = 0, alreadySorted: Boolean = false) {\n        if (list.isEmpty()) return\n        player.appendDanmakus(list, maxItems, alreadySorted); invalidate()\n    }\n    fun trimToTimeRange(minPositionMs: Long, maxPositionMs: Long) { player.trimToTimeRange(minPositionMs, maxPositionMs); invalidate() }\n    fun notifySeek(positionMs: Long) {\n        player.seekTo(positionMs)\n        lastRawPositionMs = positionMs\n        lastPositionChangeUptimeMs = SystemClock.uptimeMillis()\n        invalidate()\n    }\n\n    fun play() {\n        invalidate()\n    }\n\n    /** 显式释放资源。可多次调用，幂等。 */\n    fun release() {\n        player.release()\n        cachedMaskBitmap?.recycle()\n        cachedMaskBitmap = null\n        cachedMaskFrame = null\n    }\n\n    fun setMaskFrame(frame: DanmakuMaskFrame?) { maskFrame = frame }\n    fun setVideoAspectRatio(ratio: Float) { videoAspectRatio = ratio }\n    fun setVideoAspectRatioType(type: VideoAspectRatio) { videoAspectRatioType = type }\n\n    override fun onAttachedToWindow() {\n        super.onAttachedToWindow()\n        updateViewportIfNeeded()\n    }\n\n    override fun onDetachedFromWindow() {\n        super.onDetachedFromWindow()\n        player.release()\n        cachedMaskBitmap?.recycle()\n        cachedMaskBitmap = null\n        cachedMaskFrame = null\n    }\n\n    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {\n        super.onSizeChanged(w, h, oldw, oldh)\n        updateViewportIfNeeded()\n    }\n\n    override fun onDraw(canvas: Canvas) {\n        super.onDraw(canvas)\n\n        updateViewportIfNeeded()\n        if (!config.enabled) {\n            player.draw(canvas, 0L, false, 1f, config); return\n        }\n\n        val posProvider = positionProvider ?: return\n        val rawPos = posProvider()\n        val now = SystemClock.uptimeMillis()\n        if (lastPositionChangeUptimeMs == 0L) lastPositionChangeUptimeMs = now\n        if (rawPos != lastRawPositionMs) lastPositionChangeUptimeMs = now\n        lastRawPositionMs = rawPos\n\n        val fallbackPlaying = now - lastPositionChangeUptimeMs < STOP_WHEN_IDLE_MS\n        val playingProvider = isPlayingProvider\n        val isPlaying = if (playingProvider != null) {\n            try { playingProvider() } catch (_: Exception) { fallbackPlaying }\n        } else {\n            fallbackPlaying\n        }\n        val speedProvider = playbackSpeedProvider\n        val speed = if (speedProvider != null) {\n            val candidate = try { speedProvider() } catch (_: Exception) { Float.NaN }\n            if (candidate.isFinite() && candidate > 0f) candidate else 1f\n        } else {\n            1f\n        }\n\n        // DstIn blending for mask requires an offscreen buffer — use hardware layer only when needed.\n        val mask = maskFrame\n        val needsHwLayer = mask != null\n        val desiredLayerType = if (needsHwLayer) LAYER_TYPE_HARDWARE else LAYER_TYPE_NONE\n        if (desiredLayerType != currentLayerType) {\n            setLayerType(desiredLayerType, null)\n            currentLayerType = desiredLayerType\n        }\n\n        player.draw(canvas, rawPos, isPlaying, speed, config)\n\n        if (mask != null) {\n            val maskBitmap = getOrBuildMaskBitmap(mask)\n            if (maskBitmap != null) drawMaskBitmap(canvas, maskBitmap, videoAspectRatio, videoAspectRatioType)\n        }\n    }\n\n    private fun getOrBuildMaskBitmap(frame: DanmakuMaskFrame): Bitmap? {\n        if (frame == cachedMaskFrame && cachedMaskBitmap != null) return cachedMaskBitmap\n        cachedMaskBitmap?.recycle()\n        cachedMaskFrame = frame\n        cachedMaskBitmap = try {\n            when (frame) {\n                is DanmakuWebMaskFrame -> buildWebMaskBitmap(frame)\n                is DanmakuMobMaskFrame -> buildMobMaskBitmap(frame)\n            }\n        } catch (_: Exception) { null }\n        return cachedMaskBitmap\n    }\n\n    /** Web 蒙版：使用 androidsvg 库解析完整 SVG，渲染到 Bitmap */\n    private fun buildWebMaskBitmap(frame: DanmakuWebMaskFrame): Bitmap? {\n        val svg = frame.svg\n        if (svg.isBlank()) return null\n        val svgObj = SVG.getFromString(svg)\n        val svgW = svgObj.documentWidth.toInt()\n        val svgH = svgObj.documentHeight.toInt()\n        if (svgW <= 0 || svgH <= 0) return null\n        val bitmap = Bitmap.createBitmap(svgW, svgH, Bitmap.Config.ARGB_8888)\n        svgObj.renderToCanvas(Canvas(bitmap))\n        return bitmap\n    }\n\n    /** Mob 蒙版：40×180 1bpp 二值图，批量 setPixels 写入 */\n    private fun buildMobMaskBitmap(frame: DanmakuMobMaskFrame): Bitmap? {\n        val w = frame.width\n        val h = frame.height\n        if (w <= 0 || h <= 0) return null\n        val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)\n        val pixels = IntArray(w * h) { i ->\n            val byteIndex = i / 8\n            val bitOffset = 7 - (i % 8)\n            val bit = (frame.image[byteIndex].toInt() shr bitOffset) and 1\n            if (bit == 0) Color.TRANSPARENT else Color.BLACK\n        }\n        bitmap.setPixels(pixels, 0, w, 0, 0, w, h)\n        return bitmap\n    }\n\n    /**\n     * 将蒙版 Bitmap 以 DstIn 模式绘制到 canvas 上，正确处理视频 letterbox/pillarbox。\n     * 逻辑与 DanmakuMaskModifiers.bitmapMask 一致。\n     */\n    private fun drawMaskBitmap(\n        canvas: Canvas,\n        bitmap: Bitmap,\n        videoAspect: Float,\n        aspectType: VideoAspectRatio,\n    ) {\n        val screenW = width.toFloat()\n        val screenH = height.toFloat()\n        if (screenW <= 0f || screenH <= 0f) return\n        val screenAspect = screenW / screenH\n\n        val dstW: Float\n        val dstH: Float\n        val offsetX: Float\n        val offsetY: Float\n\n        val ratio = if (videoAspect > 0f) videoAspect else 16f / 9f\n        when (aspectType) {\n            VideoAspectRatio.Stretch -> {\n                dstW = screenW\n                dstH = screenH\n                offsetX = 0f\n                offsetY = 0f\n            }\n\n            VideoAspectRatio.EqualWidth -> {\n                dstW = screenW\n                dstH = dstW / ratio\n                offsetX = 0f\n                offsetY = (screenH - dstH) / 2f\n            }\n\n            VideoAspectRatio.EqualHeight -> {\n                dstH = screenH\n                dstW = dstH * ratio\n                offsetY = 0f\n                offsetX = (screenW - dstW) / 2f\n            }\n\n            else -> {\n                if (ratio > screenAspect) {\n                    dstW = screenW\n                    dstH = dstW / ratio\n                    offsetX = 0f\n                    offsetY = (screenH - dstH) / 2f\n                } else {\n                    dstH = screenH\n                    dstW = dstH * ratio\n                    offsetY = 0f\n                    offsetX = (screenW - dstW) / 2f\n                }\n            }\n        }\n\n        maskDstRect.set(offsetX.toInt(), offsetY.toInt(), (offsetX + dstW).toInt(), (offsetY + dstH).toInt())\n        canvas.drawBitmap(bitmap, null, maskDstRect, maskPaint)\n    }\n\n    private fun updateViewportIfNeeded() {\n        val w = width.coerceAtLeast(0); val h = height.coerceAtLeast(0)\n        val top = viewportTopInsetPx; val bottom = viewportBottomInsetPx\n        if (w == lastViewportW && h == lastViewportH && top == lastViewportTopInset && bottom == lastViewportBottomInset) return\n        lastViewportW = w; lastViewportH = h; lastViewportTopInset = top; lastViewportBottomInset = bottom\n        player.onViewportChanged(w, h, top, bottom)\n    }\n\n    private fun dp(v: Float): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, v, resources.displayMetrics).toInt()\n\n    private companion object {\n        private val DEFAULT_CONFIG = DanmakuConfig()\n        const val STOP_WHEN_IDLE_MS = 700L\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/model/Danmaku.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku.model\n\ndata class Danmaku(\n    val dmid: Long,\n    val positionMs: Int,\n    val text: String,\n    val mode: Int,\n    val textSize: Int,\n    val color: Int,\n    val level: Int = 0,\n) : Comparable<Danmaku> {\n    override fun compareTo(other: Danmaku): Int = positionMs.compareTo(other.positionMs)\n\n    companion object {\n        const val MODE_SCROLL = 1\n        const val MODE_BOTTOM = 4\n        const val MODE_TOP = 5\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/model/DanmakuItem.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku.model\n\nimport android.graphics.Bitmap\n\ninternal enum class DanmakuCacheState {\n    Init,\n    Rendering,\n    Rendered,\n}\n\ninternal class DanmakuItem(\n    val data: Danmaku,\n) {\n    @Volatile var cacheBitmap: Bitmap? = null\n    @Volatile var cacheGeneration: Int = -1\n    @Volatile var cacheState: DanmakuCacheState = DanmakuCacheState.Init\n\n    // Active state (action thread only)\n    var isActive: Boolean = false\n    var kind: DanmakuKind = DanmakuKind.SCROLL\n    var lane: Int = 0\n    var startTimeMs: Int = 0\n    var durationMs: Int = 0\n    var pxPerMs: Float = 0f\n    var textWidthPx: Float = 0f\n\n    fun timeMs(): Int = data.positionMs\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/model/DanmakuKind.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku.model\n\ninternal enum class DanmakuKind {\n    SCROLL,\n    TOP,\n    BOTTOM,\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/danmaku/model/RenderSnapshot.kt",
    "content": "package dev.aaa1115910.bv.player.danmaku.model\n\ninternal class RenderSnapshot(\n    var positionMs: Double = 0.0,\n    initialCapacity: Int = 256,\n) {\n    var items: Array<DanmakuItem?> = arrayOfNulls(initialCapacity)\n        private set\n    var yTop: FloatArray = FloatArray(initialCapacity)\n        private set\n\n    var count: Int = 0\n\n    fun ensureCapacity(required: Int) {\n        if (required <= items.size) return\n        val cap = required.coerceAtLeast(items.size * 2 + 8)\n        items = arrayOfNulls(cap)\n        yTop = FloatArray(cap)\n    }\n\n    fun clear() {\n        for (i in 0 until count) items[i] = null\n        count = 0\n        positionMs = 0.0\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/Audio.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class Audio(val code: Int, private val strRes: Int) {\n    A64K(30216, R.string.audio_64k),\n    A132K(30232, R.string.audio_132k),\n    A192K(30280, R.string.audio_192k),\n    AHiRes(30251, R.string.audio_hi_res),\n    ADolbyAtoms(30250, R.string.audio_dolby_atoms);\n\n    companion object {\n        fun fromCode(code: Int) = runCatching {\n            entries.find { it.code == code }\n        }.getOrDefault(A64K)\n    }\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/ControllerButtonConfig.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\n/**\n * 播放器控制栏按钮配置\n * @param id 按钮 ID\n * @param hidden 是否被用户隐藏\n * @param isDefaultFocus 是否为默认焦点按钮\n */\ndata class ControllerButtonConfig(\n    val id: String,\n    val hidden: Boolean = false,\n    val isDefaultFocus: Boolean = false\n)\n\n/**\n * 所有控制栏按钮 ID（默认顺序）\n */\nval ALL_CONTROLLER_BUTTON_IDS = listOf(\n    \"nextVideo\", \"refresh\", \"speed\", \"resolution\", \"upSpace\", \"rotation\",\n    \"subtitle\", \"comment\", \"danmaku\", \"playlist\", \"related\", \"description\",\n    \"playMode\", \"settings\"\n)\n\n/**\n * 解析控制栏按钮配置字符串\n *\n * 格式：逗号分隔的按钮 ID，前缀含义：\n * - 无前缀：可见\n * - `-` 前缀：隐藏\n * - `*` 前缀：默认焦点\n * - `*-` 前缀：隐藏且为默认焦点\n *\n * 例如：\"refresh,speed,-rotation,*danmaku\"\n */\nfun parseControllerButtonsOrder(orderString: String): List<ControllerButtonConfig> {\n    if (orderString.isBlank()) return emptyList()\n    val configs = orderString.split(\",\")\n        .mapNotNull { token ->\n            val trimmed = token.trim()\n            if (trimmed.isEmpty()) return@mapNotNull null\n            val isDefaultFocus = trimmed.startsWith(\"*\")\n            val afterStar = if (isDefaultFocus) trimmed.substring(1) else trimmed\n            val isHidden = afterStar.startsWith(\"-\")\n            val rawId = if (isHidden) afterStar.substring(1) else afterStar\n            // 向后兼容：旧配置中的 \"loop\" 映射为 \"playMode\"\n            val id = if (rawId == \"loop\") \"playMode\" else rawId\n            if (id.isEmpty() || !ALL_CONTROLLER_BUTTON_IDS.contains(id)) return@mapNotNull null\n            ControllerButtonConfig(id, isHidden, isDefaultFocus)\n        }\n    return insertMissingButtons(configs)\n}\n\n/**\n * 将缺失的新按钮按默认顺序插入到对应位置\n */\nprivate fun insertMissingButtons(configs: List<ControllerButtonConfig>): List<ControllerButtonConfig> {\n    val existingIds = configs.map { it.id }.toSet()\n    val missingIds = ALL_CONTROLLER_BUTTON_IDS.filter { it !in existingIds }\n    if (missingIds.isEmpty()) return configs\n\n    val result = configs.toMutableList()\n    for (missingId in missingIds) {\n        val defaultIndex = ALL_CONTROLLER_BUTTON_IDS.indexOf(missingId)\n        var insertIndex = result.size\n        for (i in result.indices.reversed()) {\n            val idxInDefault = ALL_CONTROLLER_BUTTON_IDS.indexOf(result[i].id)\n            if (idxInDefault < defaultIndex) {\n                insertIndex = i + 1\n                break\n            }\n            if (i == 0) insertIndex = 0\n        }\n        result.add(insertIndex, ControllerButtonConfig(missingId))\n    }\n    return result\n}\n\n/**\n * 将控制栏按钮配置列表序列化为字符串\n */\nfun serializeControllerButtonsOrder(configs: List<ControllerButtonConfig>): String {\n    return configs\n        .filter { ALL_CONTROLLER_BUTTON_IDS.contains(it.id) }\n        .joinToString(\",\") { config ->\n            buildString {\n                if (config.isDefaultFocus) append(\"*\")\n                if (config.hidden) append(\"-\")\n                append(config.id)\n            }\n        }\n}\n\n/**\n * 获取用于编辑的完整按钮配置列表\n * 如果存储的配置为空，返回所有按钮的默认配置；\n * 如果有值，复用 parseControllerButtonsOrder（已自动补充缺失按钮）。\n */\nfun getControllerButtonConfigsForEditing(orderString: String): List<ControllerButtonConfig> {\n    val configs = parseControllerButtonsOrder(orderString)\n    if (configs.isEmpty()) {\n        return ALL_CONTROLLER_BUTTON_IDS.map { ControllerButtonConfig(it) }\n    }\n    return configs\n}\n\n/**\n * 获取按钮的中文显示名称\n */\nfun getControllerButtonDisplayName(id: String): String {\n    return when (id) {\n        \"nextVideo\" -> \"下一个视频\"\n        \"refresh\" -> \"刷新\"\n        \"speed\" -> \"播放速度\"\n        \"resolution\" -> \"画质\"\n        \"upSpace\" -> \"UP主空间\"\n        \"rotation\" -> \"画面旋转\"\n        \"subtitle\" -> \"字幕\"\n        \"comment\" -> \"评论\"\n        \"danmaku\" -> \"弹幕\"\n        \"playMode\" -> \"播放模式\"\n        \"playlist\" -> \"播放列表\"\n        \"related\" -> \"相关推荐\"\n        \"description\" -> \"简介\"\n        \"settings\" -> \"设置\"\n        else -> id\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/DanmakuSize.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nenum class DanmakuSize(val scale: Float) {\n    S1(0.25f), S2(0.5f), S3(0.6f), S4(0.7f), S5(0.8f), S6(0.9f), S7(1f),\n    S8(1.1f), S9(1.2f), S10(1.3f), S11(1.4f), S12(1.5f), S13(2f);\n\n    companion object {\n        fun fromOrdinal(ordinal: Int) = runCatching { entries[ordinal] }\n            .getOrDefault(S2)\n    }\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/DanmakuTransparency.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nenum class DanmakuTransparency(val transparency: Float) {\n    T1(1f), T2(0.9f), T3(0.8f), T4(0.7f), T5(0.6f),\n    T6(0.5f), T7(0.3f), T8(0.2f), T9(0.1f);\n\n    companion object {\n        fun fromOrdinal(ordinal: Int) = runCatching { entries[ordinal] }\n            .getOrDefault(T1)\n    }\n\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/DanmakuType.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class DanmakuType(private val strRes: Int) {\n    All(R.string.video_player_menu_danmaku_type_all),\n    Top(R.string.video_player_menu_danmaku_type_top),\n    Rolling(R.string.video_player_menu_danmaku_type_cross),\n    Bottom(R.string.video_player_menu_danmaku_type_bottom);\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/DefaultStartPosition.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\n/**\n * 播放默认开始位置\n */\nenum class DefaultStartPosition {\n    /** 从历史位置开始 */\n    History,\n    /** 从开头开始 */\n    Beginning\n}\n\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/LiveCodec.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\n/**\n * 直播编码格式选项\n *\n * @property protocolName 协议名称，对应 B站 API 返回的 protocolName\n * @property codecName 编码名称，null 表示自动选择最佳编码\n */\nenum class LiveCodec(private val strRes: Int, val protocolName: String, val codecName: String?) {\n    HLS(R.string.live_codec_hls, \"http_hls\", null),      // HLS 自动选择最佳编码\n    AVC(R.string.live_codec_avc, \"http_hls\", \"avc\"),     // HLS 强制 AVC\n    FLV(R.string.live_codec_flv, \"http_stream\", \"avc\");  // FLV 固定 AVC\n\n    companion object {\n        fun fromCode(code: Int) = runCatching {\n            entries.find { it.ordinal == code }!!\n        }.getOrDefault(HLS)\n    }\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/PlayMode.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class PlayMode(private val strRes: Int) {\n    //单视频（播完即停）\n    SingleVideo(R.string.play_mode_single_video),\n\n    //单视频循环\n    SingleLoop(R.string.play_mode_single_loop),\n\n    //合集/分P\n    PartAndEpisode(R.string.play_mode_part_episode),\n\n    //合集/分P-逆序\n    PartAndEpisodeReverse(R.string.play_mode_part_episode_reverse),\n\n    //UGC视频列表顺序播放\n    ListOrder(R.string.play_mode_list_order),\n\n    //UGC视频列表逆序播放\n    ListOrderReverse(R.string.play_mode_list_order_reverse),\n\n    //推荐视频\n    RelatedVideo(R.string.play_mode_related_video),\n\n    //自定义（按设置中的策略顺序）\n    Custom(R.string.play_mode_custom);\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/PortraitVideoFixMode.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\n/**\n * 竖屏视频修复模式。\n * - None: 不做任何处理\n * - LimitResolution1080P: 播放竖屏视频时自动限制分辨率不高于1080P\n * - UseTextureView: 强制使用 TextureView 渲染竖屏视频\n */\nenum class PortraitVideoFixMode(val value: Int) {\n    None(0),\n    LimitResolution1080P(1),\n    UseTextureView(2);\n\n    fun displayName(context: Context): String = when (this) {\n        None -> context.getString(R.string.pvf_mode_none)\n        LimitResolution1080P -> context.getString(R.string.pvf_mode_limit_1080p)\n        UseTextureView -> context.getString(R.string.pvf_mode_use_texture_view)\n    }\n\n    companion object {\n        fun fromValue(value: Int): PortraitVideoFixMode = entries.find { it.value == value } ?: None\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/RequestState.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nenum class RequestState {\n    Ready, Doing, Done, Success, Failed\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/Resolution.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class Resolution(val code: Int, private val strResLong: Int, private val strResShort: Int) {\n    R240P(6, R.string.resolution_240p, R.string.resolution_240p_short),\n    R360P(16, R.string.resolution_360p, R.string.resolution_360p_short),\n    R480P(32, R.string.resolution_480p, R.string.resolution_480p_short),\n    R720P(64, R.string.resolution_720p, R.string.resolution_720p_short),\n    R720P60(74, R.string.resolution__720p_60, R.string.resolution_720p_60_short),\n    R1080P(80, R.string.resolution_1080p, R.string.resolution_1080p_short),\n    R1080PPlus(112, R.string.resolution_1080p_plus, R.string.resolution_1080p_plus_short),\n    R1080P60(116, R.string.resolution_1080p_60, R.string.resolution_1080p_60_short),\n    R4K(120, R.string.resolution_4k, R.string.resolution_4k_short),\n    RHdr(125, R.string.resolution_hdr, R.string.resolution_hdr_short),\n    RDolby(126, R.string.resolution_dolby_vision, R.string.resolution_dolby_bision_short),\n    R8K(127, R.string.resolution_8k, R.string.resolution_8k_short);\n\n    companion object {\n        fun fromCode(code: Int) = runCatching {\n            entries.find { it.code == code }\n        }.getOrDefault(R1080P)\n    }\n\n    fun getDisplayName(context: Context) = context.getString(strResLong)\n    fun getShortDisplayName(context: Context) = context.getString(strResShort)\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/VideoAspectRatio.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class VideoAspectRatio(private val strRes: Int) {\n    Default(R.string.video_aspect_ratio_default),\n    FourToThree(R.string.video_aspect_ratio_four_to_three),\n    SixteenToNine(R.string.video_aspect_ratio_sixteen_to_nine),\n    NineToSixteen(R.string.video_aspect_ratio_nine_to_sixteen),\n    EqualWidth(R.string.video_aspect_ratio_equal_width),\n    EqualHeight(R.string.video_aspect_ratio_equal_height),\n    Stretch(R.string.video_aspect_ratio_stretch);\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n\n    fun resolveAspectRatio(defaultAspectRatio: Float): Float {\n        val fallbackAspectRatio = defaultAspectRatio.takeIf { it > 0f } ?: (16f / 9f)\n        return when (this) {\n            Default,\n            EqualWidth,\n            EqualHeight,\n            Stretch -> fallbackAspectRatio\n\n            FourToThree -> 4f / 3f\n            SixteenToNine -> 16f / 9f\n            NineToSixteen -> 9f / 16f\n        }\n    }\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/VideoCodec.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.biliapi.entity.CodeType\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class VideoCodec(private val strRes: Int, val prefix: String, val codecId: Int) {\n    AVC(R.string.video_codec_avc, \"avc1\", 7),\n    HEVC(R.string.video_codec_hevc, \"hev1\", 12),\n    AV1(R.string.video_codec_av1, \"av01\", 13),\n    DVH1(R.string.video_codec_dvh1, \"dvh1\", 0),\n    HVC1(R.string.video_codec_hvc1, \"hvc\", 0);\n\n    companion object {\n        fun fromCode(code: Int?) = runCatching {\n            entries.find { it.ordinal == code }!!\n        }.getOrDefault(AVC)\n\n        fun fromCodecString(codec: String) = runCatching {\n            entries.forEach {\n                if (codec.startsWith(it.prefix)) return@runCatching it\n            }\n            return@runCatching null\n        }.getOrNull()\n\n        fun fromCodecId(codecId: Int) = runCatching {\n            entries.find { it.codecId == codecId }!!\n        }.getOrDefault(AVC)\n    }\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n\n    fun toBiliApiCodeType() = when (this) {\n        AVC -> CodeType.Code264\n        HEVC -> CodeType.Code265\n        AV1 -> CodeType.CodeAv1\n        DVH1, HVC1 -> CodeType.Code265\n    }\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/VideoListItem.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\ninterface VideoListItem\n\nopen class VideoListItemData(\n    open val aid: Long,\n    open val cid: Long? = null,\n    open val epid: Int? = null,\n    open val seasonId: Int? = null,\n    open val title: String,\n    open val partTitle: String = \"\",\n    open val index: Int,\n    open val cover: String = \"\",\n    open val duration: Int = 0,\n    open val pubDate: Long = 0L,\n) : VideoListItem\n\ndata class VideoListPart(\n    override val aid: Long,\n    override val cid: Long,\n    override val epid: Int? = null,\n    override val seasonId: Int? = null,\n    override val title: String,\n    override val partTitle: String = \"\",\n    override val index: Int,\n    override val cover: String = \"\",\n    override val duration: Int = 0,\n    override val pubDate: Long = 0L,\n) : VideoListItemData(aid, cid, epid, seasonId, title, partTitle, index, cover, duration, pubDate)\n\ndata class VideoListUgcEpisode(\n    override val aid: Long,\n    override val cid: Long,\n    override val epid: Int? = null,\n    override val seasonId: Int? = null,\n    override val title: String,\n    override val partTitle: String = \"\",\n    override val index: Int,\n    override val cover: String = \"\",\n    override val duration: Int = 0,\n    override val pubDate: Long = 0L,\n) : VideoListItemData(aid, cid, epid, seasonId, title, partTitle, index, cover, duration, pubDate)\n\ndata class VideoListUgcEpisodeTitle(\n    val index: Int,\n    val title: String\n) : VideoListItem\n\ndata class VideoListPgcEpisode(\n    override val aid: Long,\n    override val cid: Long,\n    override val epid: Int? = null,\n    override val seasonId: Int? = null,\n    override val title: String,\n    override val partTitle: String = \"\",\n    override val index: Int,\n    override val cover: String = \"\",\n    override val duration: Int = 0,\n    override val pubDate: Long = 0L,\n) : VideoListItemData(aid, cid, epid, seasonId, title, partTitle, index, cover, duration, pubDate)\n\ndata class VideoListOtherVideo(\n    override val aid: Long,\n    override val cid: Long? = null,\n    override val epid: Int? = null,\n    override val seasonId: Int? = null,\n    override val title: String,\n    override val partTitle: String = \"\",\n    override val index: Int,\n    override val cover: String = \"\",\n    val upName: String = \"\"\n) : VideoListItemData(aid, cid, epid, seasonId, title, partTitle, index, cover)"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/VideoPlayerClosedCaptionMenuItem.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class VideoPlayerClosedCaptionMenuItem(private val strRes: Int) {\n    Switch(R.string.video_player_menu_subtitle_switch),\n    Size(R.string.video_player_menu_subtitle_size),\n    Opacity(R.string.video_player_menu_subtitle_background_opacity),\n    Padding(R.string.video_player_menu_subtitle_bottom_padding);\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/VideoPlayerDanmakuMenuItem.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class VideoPlayerDanmakuMenuItem(private val strRes: Int) {\n    Switch(R.string.video_player_menu_danmaku_switch),\n    RollingDurationFactor(R.string.video_player_menu_danmaku_rolling_duration_factor),\n    Size(R.string.video_player_menu_danmaku_size),\n    Opacity(R.string.video_player_menu_danmaku_opacity),\n    Area(R.string.video_player_menu_danmaku_area),\n    Mask(R.string.video_player_menu_danmaku_mask),\n    FilterLevel(R.string.video_player_menu_danmaku_filter_level);\n\n    fun getDisplayName(context: Context, isLive: Boolean = false): String = when {\n        this == FilterLevel && isLive -> context.getString(R.string.video_player_menu_danmaku_filter_user_level)\n        else -> context.getString(strRes)\n    }\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/VideoPlayerData.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskSegment\nimport dev.aaa1115910.biliapi.http.entity.video.ClipInfo\nimport dev.aaa1115910.biliapi.entity.video.Subtitle\nimport dev.aaa1115910.biliapi.entity.video.VideoShot\nimport dev.aaa1115910.bilisubtitle.entity.SubtitleItem\n\ndata class VideoPlayerSeekData(\n    val duration: Long = 1,\n    val position: Long = 0,\n    val bufferedPercentage: Int = 0\n)\n\n@Stable\nclass VideoPlayerSeekState(\n    duration: Long = 1,\n    position: Long = 0,\n    bufferedPercentage: Int = 0\n) {\n    var duration by mutableLongStateOf(duration)\n    var position by mutableLongStateOf(position)\n    var bufferedPercentage by mutableIntStateOf(bufferedPercentage)\n}\n\ndata class VideoPlayerSeekThumbData(\n    val idleIcon: String = \"\",\n    val movingIcon: String = \"\",\n)\n\ndata class VideoPlayerVideoInfoData(\n    val width: Int = 0,\n    val height: Int = 0,\n    val codec: String = \"\",\n    val title: String = \"Title\",\n    val partTitle: String = \"PartTitle\",\n    val play: Long = 0,\n    val danmaku: Int = 0,\n    val like: Int = 0,\n    val coin: Int = 0,\n    val favorite: Int = 0,\n    val upName: String = \"\",\n    val pubTime: String = \"\",\n    val fromSeason: Boolean = false,\n    val isFollowingUp: Boolean = false,\n    val isVerticalVideo: Boolean = false,\n    val isLive: Boolean = false\n)\n\ndata class VideoPlayerClockData(\n    val hour: Int = 0,\n    val minute: Int = 0,\n    val second: Int = 0,\n)\n\n@Stable\nclass VideoPlayerClockState(\n    hour: Int = 0,\n    minute: Int = 0,\n    second: Int = 0\n) {\n    var hour by mutableIntStateOf(hour)\n    var minute by mutableIntStateOf(minute)\n    var second by mutableIntStateOf(second)\n}\n\ndata class VideoPlayerLogsData(\n    val logs: String = \"\",\n)\n\ndata class VideoPlayerHistoryData(\n    val lastPlayed: Int = 0,\n    val showBackToHistory: Boolean = false,\n)\n\ndata class VideoPlayerPaymentData(\n    val needPay: Boolean = false,\n    val epid: Int = 0,\n    val showPreviewTip: Boolean = false,\n)\n\ndata class VideoPlayerLoadStateData(\n    val loadState: RequestState = RequestState.Ready,\n    val errorMessage: String = \"\",\n)\n\ndata class VideoPlayerStateData(\n    val isPlaying: Boolean = false,\n    val isBuffering: Boolean = false,\n    val isError: Boolean = false,\n    val exception: Exception? = null,\n    val showBackToHistory: Boolean = false,\n)\n\ndata class VideoPlayerConfigData(\n    val availableResolutions: List<Resolution> = emptyList(),\n    val availableVideoCodec: List<VideoCodec> = emptyList(),\n    val availableAudio: List<Audio> = emptyList(),\n    val availableSubtitleTracks: List<Subtitle> = emptyList(),\n    val availableVideoList: List<VideoListItem> = emptyList(),\n    val currentVideoCid: Long = 0,\n    val currentResolution: Resolution = Resolution.R240P,\n    val currentVideoCodec: VideoCodec = VideoCodec.AVC,\n    val currentVideoAspectRatio: VideoAspectRatio = VideoAspectRatio.Default,\n    val currentVideoRotation: VideoRotation = VideoRotation.Original,\n    val currentVideoSpeed: Float = 1f,\n    val currentAudio: Audio = Audio.A192K,\n    val currentDanmakuEnabled: Boolean = true,\n    val currentDanmakuEnabledList: List<DanmakuType> = listOf(),\n    val currentDanmakuSize: DanmakuSize = DanmakuSize.S2,\n    val currentDanmakuScale: Float = 1f,\n    val currentDanmakuTransparency: DanmakuTransparency = DanmakuTransparency.T1,\n    val currentDanmakuOpacity: Float = 1f,\n    val currentDanmakuArea: Float = 1f,\n    val currentDanmakuMask: Boolean = false,\n    val currentDanmakuRollingDurationFactor: Float = 1f,\n    val currentDanmakuFilterLevel: Int = 0,\n    val currentLiveDanmakuFilterLevel: Int = 0,\n    val currentSubtitleId: Long = 0,\n    val currentSubtitleData: List<SubtitleItem> = emptyList(),\n    val currentSubtitleFontSize: TextUnit = 24.sp,\n    val currentSubtitleBackgroundOpacity: Float = 0.4f,\n    val currentSubtitleBottomPadding: Dp = 12.dp,\n    val currentPlayMode: PlayMode = PlayMode.PartAndEpisode,\n    val incognitoMode: Boolean = false,\n    val hasPreloadedVideoList: Boolean = false,\n    val hasRelatedVideos: Boolean = false,\n    val fromSeason: Boolean = false,\n    var showDanmaku: Boolean = true,\n    var showRelatedVideos: Boolean = false,\n    var showNextVideoBtn: Boolean = false,\n    val defaultStartPosition: DefaultStartPosition = DefaultStartPosition.History,\n    val clipInfoList: List<ClipInfo> = emptyList(),\n    val skipPgcIntroOutro: Boolean = false,\n    val isLive: Boolean = false,\n    val availableLiveQualities: List<Pair<Int, String>> = emptyList(),\n    val currentLiveQn: Int = 0,\n    val currentLiveQualityDescription: String = \"\",\n    val controllerButtonsOrder: String = \"\",\n    val availableLiveCodecs: List<LiveCodec> = LiveCodec.entries,\n    val currentLiveCodec: LiveCodec = LiveCodec.HLS,\n    val showDebugInfo: Boolean = false,\n    val longPressAction: Int = 0, // 0 = 打开菜单, 1 = 加速播放\n    val longPressSpeed: Float = 2f,\n)\n\ndata class VideoPlayerDanmakuMasksData(\n    val danmakuMasks: List<DanmakuMaskSegment> = emptyList(),\n)\n\ndata class VideoPlayerVideoShotData(\n    val videoShot: VideoShot? = null,\n)\n\ndata class VideoPlayerDebugInfoData(\n    val debugInfo: String = \"\",\n)\n\nval LocalVideoPlayerSeekData = compositionLocalOf { VideoPlayerSeekData() }\nval LocalVideoPlayerSeekState = compositionLocalOf { VideoPlayerSeekState() }\nval LocalVideoPlayerSeekThumbData = compositionLocalOf { VideoPlayerSeekThumbData() }\nval LocalVideoPlayerVideoInfoData = compositionLocalOf { VideoPlayerVideoInfoData() }\nval LocalVideoPlayerClockData = compositionLocalOf { VideoPlayerClockData() }\nval LocalVideoPlayerClockState = compositionLocalOf { VideoPlayerClockState() }\nval LocalVideoPlayerLogsData = compositionLocalOf { VideoPlayerLogsData() }\nval LocalVideoPlayerHistoryData = compositionLocalOf { VideoPlayerHistoryData() }\nval LocalVideoPlayerPaymentData = compositionLocalOf { VideoPlayerPaymentData() }\nval LocalVideoPlayerLoadStateData = compositionLocalOf { VideoPlayerLoadStateData() }\nval LocalVideoPlayerStateData = compositionLocalOf { VideoPlayerStateData() }\nval LocalVideoPlayerConfigData = compositionLocalOf { VideoPlayerConfigData() }\nval LocalVideoPlayerDanmakuMasksData = compositionLocalOf { VideoPlayerDanmakuMasksData() }\nval LocalVideoPlayerVideoShotData = compositionLocalOf { VideoPlayerVideoShotData() }\nval LocalVideoPlayerDebugInfoData = compositionLocalOf { VideoPlayerDebugInfoData() }"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/VideoPlayerMenuNavItem.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.ClearAll\nimport androidx.compose.material.icons.outlined.ClosedCaption\nimport androidx.compose.material.icons.outlined.Image\nimport androidx.compose.material.icons.outlined.MoreVert\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class VideoPlayerMenuNavItem(private val strRes: Int, val icon: ImageVector) {\n    Picture(R.string.video_player_menu_nav_picture, Icons.Outlined.Image),\n    Danmaku(R.string.video_player_menu_nav_danmaku, Icons.Outlined.ClearAll),\n    ClosedCaption(R.string.video_player_menu_nav_subtitle, Icons.Outlined.ClosedCaption),\n    Others(R.string.video_player_menu_nav_others, Icons.Outlined.MoreVert);\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/VideoPlayerOthersMenuItem.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class VideoPlayerOthersMenuItem(private val strRes: Int) {\n    PlayMode(R.string.video_player_menu_others_play_mode),\n    DebugInfo(R.string.video_player_menu_others_debug_info);\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/VideoPlayerPictureMenuItem.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class VideoPlayerPictureMenuItem(private val strRes: Int) {\n    PlaySpeed(R.string.video_player_menu_picture_play_speed),\n    Resolution(R.string.video_player_menu_picture_resolution),\n    Codec(R.string.video_player_menu_picture_codec),\n    Audio(R.string.video_player_menu_picture_audio),\n    Rotation(R.string.video_player_menu_picture_rotation),\n    AspectRatio(R.string.video_player_menu_picture_aspect_ratio);\n\n    fun getDisplayName(context: Context) = context.getString(strRes)\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/entity/VideoRotation.kt",
    "content": "package dev.aaa1115910.bv.player.entity\n\nimport android.content.Context\nimport dev.aaa1115910.bv.player.shared.R\n\nenum class VideoRotation(val degrees: Float, private val labelRes: Int) {\n    Original(0f, R.string.video_rotation_original),\n    Rotate90(90f, R.string.video_rotation_90),\n    RotateNeg90(-90f, R.string.video_rotation_negative_90),\n    Rotate180(180f, R.string.video_rotation_180);\n\n    fun getDisplayName(context: Context): String = context.getString(labelRes)\n\n    val shouldSwapDimensions: Boolean\n        get() = degrees % 180f != 0f\n\n    companion object {\n        fun fromDegrees(degrees: Float): VideoRotation = when (degrees.toInt()) {\n            90 -> Rotate90\n            -90 -> RotateNeg90\n            180, -180 -> Rotate180\n            else -> Original\n        }\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/seekbar/SeekBar.kt",
    "content": "package dev.aaa1115910.bv.player.seekbar\n\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animateFloat\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.rememberInfiniteTransition\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SliderColors\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.drawscope.drawIntoCanvas\nimport androidx.compose.ui.graphics.drawscope.inset\nimport androidx.compose.ui.graphics.nativeCanvas\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlin.math.PI\nimport kotlin.math.max\nimport kotlin.math.sin\n\n@Composable\nfun WavySeekBar(\n    modifier: Modifier = Modifier,\n    duration: Long,\n    position: Long,\n    bufferedPercentage: Int,\n    waving: Boolean = true,\n    showThumb: Boolean = true,\n    colors: SliderColors = SliderDefaults.colors(),\n) {\n    val trackWidth = 10f\n\n    val infiniteTransition = rememberInfiniteTransition(label = \"wave\")\n    val wavingPhase by infiniteTransition.animateFloat(\n        initialValue = 0f,\n        targetValue = (2 * PI).toFloat(),\n        animationSpec = infiniteRepeatable(\n            animation = tween(\n                durationMillis = (1000 / 0.3f).toInt(),\n                easing = LinearEasing\n            ),\n            repeatMode = RepeatMode.Restart\n        ),\n        label = \"phase\"\n    )\n\n    var pausedPhase by remember { mutableFloatStateOf(0f) }\n    val phase by remember(waving) {\n        pausedPhase = wavingPhase\n        derivedStateOf {\n            if (waving) {\n                wavingPhase\n            } else {\n                pausedPhase\n            }\n        }\n    }\n\n    val ampPx by animateFloatAsState(\n        targetValue = if (waving) 6f else 0f,\n        animationSpec = spring(\n            dampingRatio = Spring.DampingRatioNoBouncy,\n            stiffness = Spring.StiffnessVeryLow\n        ),\n        label = \"amplitude\"\n    )\n\n    Canvas(\n        modifier = modifier\n            .fillMaxWidth()\n            .height(18.dp)\n    ) {\n        inset(horizontal = 0f, vertical = 0f) {\n            val shadowPaint = android.graphics.Paint().apply {\n                color = android.graphics.Color.BLACK\n                alpha = (0.3f * 255).toInt() // 设置透明度\n                setShadowLayer(8f, 4f, 4f, android.graphics.Color.BLACK) // 模糊半径和偏移\n            }\n\n            // background track shadow\n            drawIntoCanvas { canvas ->\n                canvas.nativeCanvas.drawLine(\n                    size.width * (position / duration.toFloat()),\n                    center.y,\n                    size.width,\n                    center.y,\n                    shadowPaint\n                )\n            }\n\n            // background track\n            drawLine(\n                color = colors.inactiveTrackColor,\n                start = Offset(size.width * (position / duration.toFloat()), center.y),\n                end = Offset(size.width - 0f, center.y),\n                strokeWidth = trackWidth,\n                cap = StrokeCap.Round\n            )\n\n            // buffered track\n            val bufferedTrackEnd = max(position / duration.toFloat(), bufferedPercentage / 100f)\n            drawLine(\n                color = colors.disabledActiveTrackColor,\n                start = Offset(size.width * (position / duration.toFloat()), center.y),\n                end = Offset(size.width * bufferedTrackEnd, center.y),\n                strokeWidth = trackWidth,\n                cap = StrokeCap.Round\n            )\n\n            // animated wave line shadow\n            val waveLenPx = 80f\n            val step = 2f\n            val height = size.height / 2\n            val shadowAmpPx = ampPx + 4f // 阴影的振幅稍大\n            var prevShadowX = 0f\n            var prevShadowY = height + shadowAmpPx * sin(phase)\n            drawIntoCanvas { canvas ->\n                for (x in step.toInt()..(size.width * (position / duration.toFloat())).toInt() step step.toInt()) {\n                    val shadowY =\n                        height + shadowAmpPx * sin(2 * PI * x / waveLenPx + phase).toFloat()\n                    canvas.nativeCanvas.drawLine(\n                        prevShadowX,\n                        prevShadowY,\n                        x.toFloat(),\n                        shadowY,\n                        shadowPaint\n                    )\n                    prevShadowX = x.toFloat()\n                    prevShadowY = shadowY\n                }\n            }\n\n            // animated wave line\n            var prevX = 0f\n            var prevY = height + ampPx * sin(phase)\n            for (x in step.toInt()..(size.width * (position / duration.toFloat())).toInt() step step.toInt()) {\n                val y = height + ampPx * sin(2 * PI * x / waveLenPx + phase).toFloat()\n                drawLine(\n                    color = colors.activeTrackColor,\n                    start = Offset(prevX, prevY),\n                    end = Offset(x.toFloat(), y),\n                    strokeWidth = trackWidth,\n                    cap = StrokeCap.Round\n                )\n                prevX = x.toFloat()\n                prevY = y\n            }\n\n            // thumb indicator\n            if (showThumb) {\n                val thumbX = size.width * (position / duration.toFloat())\n                val thumbHeight = 40f\n                val thumbWidth = 14f\n\n                drawLine(\n                    color = colors.activeTrackColor,\n                    start = Offset(thumbX, (size.height - thumbHeight) / 2),\n                    end = Offset(thumbX, (size.height + thumbHeight) / 2),\n                    strokeWidth = thumbWidth,\n                    cap = StrokeCap.Round\n                )\n            }\n        }\n    }\n}\n\n\n@Composable\nfun SeekBar(\n    modifier: Modifier = Modifier,\n    duration: Long,\n    position: Long,\n    bufferedPercentage: Int,\n    colors: SliderColors = SliderDefaults.colors(),\n    height: Dp = 10.dp,\n    strokeWidth: Dp = height\n) {\n    val density = LocalDensity.current\n    val strokeWidthPx = with(density) { strokeWidth.roundToPx().toFloat() }\n    Canvas(\n        modifier = modifier\n            .fillMaxWidth()\n            .height(height)\n    ) {\n        drawLine(\n            color = colors.inactiveTrackColor,\n            start = Offset(0f, center.y),\n            end = Offset(size.width - 0f, center.y),\n            strokeWidth = strokeWidthPx,\n            cap = StrokeCap.Round\n        )\n        drawLine(\n            color = colors.disabledInactiveTrackColor,\n            start = Offset(0f, center.y),\n            end = Offset(size.width * bufferedPercentage / 100, center.y),\n            strokeWidth = strokeWidthPx,\n            cap = StrokeCap.Round\n        )\n        drawLine(\n            color = colors.activeTrackColor,\n            start = Offset(0f, center.y),\n            end = Offset(size.width * (position / duration.toFloat()), center.y),\n            strokeWidth = strokeWidthPx,\n            cap = StrokeCap.Round\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun WavySeekPreview() {\n    MaterialTheme {\n        WavySeekBar(\n            modifier = Modifier.padding(horizontal = 16.dp),\n            duration = 1000,\n            position = 300,\n            bufferedPercentage = 50\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun SeekPreview() {\n    MaterialTheme {\n        SeekBar(\n            modifier = Modifier.padding(horizontal = 16.dp),\n            duration = 1000,\n            position = 300,\n            bufferedPercentage = 50\n        )\n    }\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/seekbar/SeekBarThumb.kt",
    "content": "package dev.aaa1115910.bv.player.seekbar\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Surface\nimport com.airbnb.lottie.compose.LottieAnimation\nimport com.airbnb.lottie.compose.LottieCompositionSpec\nimport com.airbnb.lottie.compose.rememberLottieComposition\n\n@Composable\nfun SeekBarThumb(\n    modifier: Modifier = Modifier,\n    state: SeekMoveState,\n    idleJsonUrl: String,\n    movingJsonUrl: String,\n    size: Dp = 48.dp\n) {\n    val idleComposition by rememberLottieComposition(\n        spec = LottieCompositionSpec.Url(idleJsonUrl)\n    )\n    val movingComposition by rememberLottieComposition(\n        spec = LottieCompositionSpec.Url(movingJsonUrl)\n    )\n\n    val idleProgress by animateFloatAsState(\n        targetValue = when (state) {\n            SeekMoveState.Idle -> 1f\n            else -> 0f\n        },\n        animationSpec = when (state) {\n            SeekMoveState.Idle -> tween(800)\n            else -> tween(0)\n        },\n        label = \"idle progress\"\n    )\n\n    val movingProgress by animateFloatAsState(\n        targetValue = when (state) {\n            SeekMoveState.Backward -> 0f\n            SeekMoveState.Forward -> 1f\n            else -> 0.5f\n        },\n        animationSpec = when (state) {\n            SeekMoveState.Backward -> tween(800)\n            SeekMoveState.Forward -> tween(800)\n            else -> tween(0)\n        },\n        label = \"moving progress\"\n    )\n\n    LottieAnimation(\n        modifier = modifier\n            .size(size),\n        composition = when (state) {\n            SeekMoveState.Backward -> movingComposition\n            SeekMoveState.Forward -> movingComposition\n            SeekMoveState.Idle -> idleComposition\n        },\n        progress = {\n            when (state) {\n                SeekMoveState.Backward -> movingProgress\n                SeekMoveState.Forward -> movingProgress\n                SeekMoveState.Idle -> idleProgress\n            }\n        }\n    )\n}\n\n@Preview\n@Composable\nprivate fun ProgressSeekThumbPreview() {\n    var state by remember { mutableStateOf(SeekMoveState.Idle) }\n    Surface {\n        Column {\n            SeekBarThumb(\n                state = state,\n                idleJsonUrl = \"https://i0.hdslb.com/bfs/garb/item/df917f079cd8175cc851cd1e19a197d810a1c6b7.json\",\n                movingJsonUrl = \"https://i0.hdslb.com/bfs/garb/item/b61bb387a4c895ef165798102ef322c631a9e4e1.json\"\n            )\n            Button(onClick = { state = SeekMoveState.Idle }) { Text(text = \"idle\") }\n            Button(onClick = { state = SeekMoveState.Backward }) { Text(text = \"backward\") }\n            Button(onClick = { state = SeekMoveState.Forward }) { Text(text = \"forward\") }\n        }\n    }\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/seekbar/SeekMoveState.kt",
    "content": "package dev.aaa1115910.bv.player.seekbar\n\nenum class SeekMoveState {\n    Backward,\n    Forward,\n    Idle\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/util/DanmakuMaskFinder.kt",
    "content": "package dev.aaa1115910.bv.player.util\n\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskFrame\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskSegment\n\n/**\n * 弹幕蒙版查找工具类\n * 使用二分查找\n *\n */\nobject DanmakuMaskFinder {\n    /**\n     * 在 segments 中定位包含指定时间的帧\n     *\n     * @param segments 蒙版 segment 列表\n     * @param positionMs 当前播放位置（毫秒）\n     * @return 包含该时间的蒙版帧，如果未找到则返回 null\n     */\n    fun findMaskFrame(\n        segments: List<DanmakuMaskSegment>,\n        positionMs: Long\n    ): DanmakuMaskFrame? {\n        if (segments.isEmpty()) return null\n\n        val segment = binarySearchSegment(segments, positionMs) ?: return null\n\n        return binarySearchFrame(segment.frames, positionMs)\n    }\n\n    /**\n     * 二分查找指定时间的 segment\n     */\n    private fun binarySearchSegment(\n        segments: List<DanmakuMaskSegment>,\n        positionMs: Long\n    ): DanmakuMaskSegment? {\n        var left = 0\n        var right = segments.size - 1\n\n        while (left <= right) {\n            val mid = (left + right) ushr 1\n            val segment = segments[mid]\n\n            when {\n                positionMs < segment.range.first -> right = mid - 1\n                positionMs > segment.range.last -> left = mid + 1\n                else -> return segment\n            }\n        }\n        return null\n    }\n\n    /**\n     * 二分查找指定时间的 frame\n     */\n    private fun binarySearchFrame(\n        frames: List<DanmakuMaskFrame>,\n        positionMs: Long\n    ): DanmakuMaskFrame? {\n        if (frames.isEmpty()) return null\n\n        var left = 0\n        var right = frames.size - 1\n\n        while (left <= right) {\n            val mid = (left + right) ushr 1\n            val frame = frames[mid]\n\n            when {\n                positionMs < frame.range.first -> right = mid - 1\n                positionMs > frame.range.last -> left = mid + 1\n                else -> return frame\n            }\n        }\n        return null\n    }\n}\n"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/util/DanmakuMaskModifiers.kt",
    "content": "package dev.aaa1115910.bv.player.util\n\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.graphics.BlendMode\nimport androidx.compose.ui.graphics.CompositingStrategy\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.toIntSize\nimport com.caverock.androidsvg.SVG\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskFrame\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMobMaskFrame\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuWebMaskFrame\n\n/**\n * 使用预转换的 ImageBitmap 进行蒙版绘制，避免每帧 Bitmap→ImageBitmap 转换开销。\n * 使用 CompositingStrategy.Offscreen 进行离屏合成，避免每帧 saveLayer 开销。\n */\nfun Modifier.bitmapMask(\n    imageBitmap: ImageBitmap,\n    videoAspectRatio: Float,\n    areaRatio: Float\n): Modifier = graphicsLayer {\n    compositingStrategy = CompositingStrategy.Offscreen\n}.drawWithContent {\n    drawContent()\n\n    val safeArea = if (areaRatio <= 0f) 1f else areaRatio\n    val screenWidth = size.width\n    val screenHeight = size.height / safeArea\n    val screenAspectRatio = screenWidth / screenHeight\n\n    val dstWidth: Float\n    val dstHeight: Float\n    val offsetX: Float\n    val offsetY: Float\n\n    if (videoAspectRatio > screenAspectRatio) {\n        dstWidth = screenWidth\n        dstHeight = dstWidth / videoAspectRatio\n\n        offsetX = 0f\n        offsetY = (screenHeight - dstHeight) / 2f\n    } else {\n        dstHeight = screenHeight\n        dstWidth = dstHeight * videoAspectRatio\n\n        offsetY = 0f\n        offsetX = (screenWidth - dstWidth) / 2f\n    }\n\n    drawImage(\n        image = imageBitmap,\n        dstOffset = IntOffset(offsetX.toInt(), offsetY.toInt()),\n        dstSize = IntSize(dstWidth.toInt(), dstHeight.toInt()),\n        blendMode = BlendMode.DstIn\n    )\n}\n\n/**\n * Web 蒙版：解析 SVG → 渲染到 Bitmap → 转换为 ImageBitmap。\n * 使用 remember(frame) 缓存结果，同一帧数据不会重复解析。\n */\nfun Modifier.danmakuWebMask(\n    frame: DanmakuWebMaskFrame,\n    videoAspectRatio: Float,\n    areaRatio: Float\n): Modifier = composed {\n    val cachedImage = remember(frame) {\n        runCatching {\n            val svgObj = SVG.getFromString(frame.svg)\n            val svgWidth = svgObj.documentWidth.toInt()\n            val svgHeight = svgObj.documentHeight.toInt()\n            if (svgWidth <= 0 || svgHeight <= 0) return@runCatching null\n            val bitmap = Bitmap.createBitmap(svgWidth, svgHeight, Bitmap.Config.ARGB_8888)\n            svgObj.renderToCanvas(Canvas(bitmap))\n            bitmap.asImageBitmap()\n        }.getOrNull()\n    } ?: return@composed this\n\n    bitmapMask(cachedImage, videoAspectRatio, areaRatio)\n}\n\n/**\n * Mob 蒙版：40×180 二值图。\n * 优化：使用 IntArray + setPixels 批量写入替代逐像素 setPixel，性能提升约 10 倍。\n */\nfun Modifier.danmakuMobMask(\n    frame: DanmakuMobMaskFrame,\n    videoAspectRatio: Float,\n    areaRatio: Float\n): Modifier = composed {\n    val cachedImage = remember(frame) {\n        val width = frame.width\n        val height = frame.height\n        val binaryBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)\n\n        // 1bpp 连续 bit 流，MSB first\n        val pixels = IntArray(width * height) { i ->\n            val byteIndex = i / 8\n            val bitOffset = 7 - (i % 8)\n            val bit = (frame.image[byteIndex].toInt() shr bitOffset) and 1\n            if (bit == 0) Color.TRANSPARENT else Color.BLACK\n        }\n        binaryBitmap.setPixels(pixels, 0, width, 0, 0, width, height)\n        binaryBitmap.asImageBitmap()\n    }\n\n    bitmapMask(cachedImage, videoAspectRatio, areaRatio)\n}\n\n/**\n * 统一蒙版入口，根据蒙版类型分发。\n * 使用 remember(frame) 确保同一帧不重复计算 Modifier 链。\n */\nfun Modifier.danmakuMask(\n    frame: DanmakuMaskFrame?,\n    videoAspectRatio: Float, // 视频的宽高比 (例如 1920/1080 ≈ 1.77, 21/9 ≈ 2.33)\n    areaRatio: Float         // 弹幕区域占屏幕高度的比例 (0.0 - 1.0)\n): Modifier = composed {\n    if (frame == null) return@composed this\n\n    when (frame) {\n        is DanmakuWebMaskFrame -> danmakuWebMask(frame, videoAspectRatio, areaRatio)\n        is DanmakuMobMaskFrame -> danmakuMobMask(frame, videoAspectRatio, areaRatio)\n    }\n}"
  },
  {
    "path": "player/shared/src/main/kotlin/dev/aaa1115910/bv/player/util/VideoShotExtends.kt",
    "content": "package dev.aaa1115910.bv.player.util\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.os.Build\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.asImageBitmap\nimport dev.aaa1115910.biliapi.entity.video.VideoShot\nimport dev.aaa1115910.biliapi.repositories.VideoPlayRepository\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.coroutineScope\n\nsuspend fun VideoShot.getImage(time: Int): Bitmap {\n    val index = findClosestValueIndex(times, time.toUShort())\n    val singleImgCount = imageCountX * imageCountY\n    val imagesIndex = index / singleImgCount\n    val imageIndex = index % singleImgCount\n    val x = imageIndex % imageCountX\n    val y = imageIndex / imageCountX\n\n    //println(\"get $time at $imagesIndex $x $y\")\n\n    // 新的支持调用去重（保留第一次解码）的缓存机制\n    val bitmap = VideoShotImageCache.getOrDecodeImage(imagesIndex, images[imagesIndex]!!)\n\n    val realImageWidth = bitmap.width / imageCountX\n    val realImageHeight = bitmap.height / imageCountY\n\n    return Bitmap.createBitmap(\n        bitmap, x * realImageWidth, y * realImageHeight, realImageWidth, realImageHeight\n    )\n}\n\nprivate fun findClosestValueIndex(array: List<UShort>, target: UShort): Int {\n    var left = 0\n    var right = array.size - 1\n    while (left < right) {\n        val mid = left + (right - left) / 2\n        if (array[mid] < target) {\n            left = mid + 1\n        } else {\n            right = mid\n        }\n    }\n    return left\n}\n\nprivate object VideoShotImageCache {\n    private data class CacheEntry(\n        val hash: Int,\n        val image: Bitmap\n    )\n\n    private const val MAX_CACHE_SIZE = 2\n\n    // 使用 LRU 缓存最多两张大图\n    private val cache = object : LinkedHashMap<Int, CacheEntry>(MAX_CACHE_SIZE, 0.75f, true) {\n        override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, CacheEntry>?): Boolean {\n            return size > MAX_CACHE_SIZE\n        }\n    }\n\n    // 保存正在解码的任务，避免重复解码\n    private val decodingTasks = mutableMapOf<Int, Deferred<Bitmap>>()\n\n    // BitmapFactory 配置，使用 RGB_565 以减少内存占用\n    val bitmapOptions = BitmapFactory.Options().apply {\n        // 内存优化\n        inPreferredConfig = Bitmap.Config.RGB_565 // 比 ARGB_8888 节省一半内存\n        inMutable = false // 不可变，节省内存\n        \n        // 解码优化\n        inScaled = false // 禁用缩放，避免额外计算\n        \n        // 内存管理\n        inTempStorage = ByteArray(16 * 1024) // 16KB临时缓冲区，减少内存分配\n        inSampleSize = 1 // 采样率，1表示原始大小\n        \n        // 其他性能优化\n        inJustDecodeBounds = false // 实际解码像素数据\n        inPremultiplied = false // 不进行预乘处理，节省计算\n        \n        // 现代化优化参数\n        inBitmap = null // 不复用现有Bitmap，避免尺寸不匹配问题\n        inDensity = 0 // 忽略密度设置，使用原始尺寸\n        inTargetDensity = 0 // 忽略目标密度\n        inScreenDensity = 0 // 忽略屏幕密度\n        \n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            inPreferredColorSpace = null // API 26+ 可使用默认色彩空间，避免转换开销\n        }\n    }\n\n    suspend fun getOrDecodeImage(imagesIndex: Int, imageData: ByteArray): Bitmap = coroutineScope {\n        val imageHash = imageData.hashCode()\n\n        // 如果已经缓存了这张图片，直接返回\n        cache[imagesIndex]?.let { entry ->\n            if (entry.hash == imageHash) {\n                return@coroutineScope entry.image\n            }\n        }\n\n        // 如果正在解码这张图片，等待解码完成\n        decodingTasks[imagesIndex]?.let { existingTask ->\n            return@coroutineScope existingTask.await()\n        }\n\n        val decodingTask = async {\n            BitmapFactory.decodeByteArray(imageData, 0, imageData.size, bitmapOptions)\n        }\n\n        decodingTasks[imagesIndex] = decodingTask\n\n        try {\n            val result = decodingTask.await()\n            cache[imagesIndex] = CacheEntry(imageHash, result)\n            return@coroutineScope result\n        } finally {\n            // 解码完成后移除任务\n            decodingTasks.remove(imagesIndex)\n        }\n    }\n}\n\n@Composable\nfun VideoShotTest(\n    modifier: Modifier = Modifier,\n    videoPlayRepository: VideoPlayRepository// = org.koin.compose.getKoin().get()\n) {\n    val aid = 170001L\n    val cid = 279786L\n    var videoShot: VideoShot? by remember { mutableStateOf(null) }\n    LaunchedEffect(Unit) {\n        videoShot = videoPlayRepository.getVideoShot(aid, cid)\n    }\n\n    if (videoShot != null) {\n        LazyVerticalGrid(\n            modifier = modifier,\n            columns = GridCells.Fixed(10),\n        ) {\n            items(videoShot!!.times) { time ->\n                var bitmap by remember { mutableStateOf<Bitmap?>(null) }\n                \n                LaunchedEffect(time) {\n                    bitmap = videoShot!!.getImage(time.toInt())\n                }\n                \n                bitmap?.let {\n                    Image(\n                        bitmap = it.asImageBitmap(),\n                        contentDescription = null\n                    )\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_danmaku_hide.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <group\n      android:scaleX=\"0.9\"\n      android:scaleY=\"0.9\"\n      android:translateX=\"51.2\"\n      android:translateY=\"51.2\">\n    <path\n        android:pathData=\"M768,544a224,224 0,1 1,0 448,224 224,0 0,1 0,-448zM832,96a96,96 0,0 1,95.6 86.8L928,192v256a32,32 0,0 1,-63.5 5.8L864,448L864,192a32,32 0,0 0,-26.2 -31.5L832,160L192,160a32,32 0,0 0,-31.5 26.2L160,192v640a32,32 0,0 0,26.2 31.5L192,864h256a32,32 0,0 1,5.8 63.5L448,928L192,928a96,96 0,0 1,-95.6 -86.8L96,832L96,192a96,96 0,0 1,86.8 -95.6L192,96h640zM634.2,680.2a160,160 0,0 0,221.9 221.4zM768,608c-32.8,0 -63.2,9.9 -88.6,26.8l222.1,221.4A160,160 0,0 0,768 608zM631,228c8,4.4 13,9.4 15,15.2 2,5.9 0.4,13.6 -5,23.1 -1.3,2.2 -2.4,4.3 -3.6,6.5l-1.9,3.3 -2.2,3.6h1.9l8.6,0.3c13.4,1 22.9,4.7 28.4,11 5.4,6.3 8.6,16.1 9.5,29.6l0.3,8.5v136.6l-0.3,8.4c-0.9,13.2 -4,23.2 -9.5,29.7 -5.4,6.6 -14.9,10.4 -28.4,11.5l-8.6,0.3h-60.1v31.9l62.8,-0.1a257.2,257.2 0,0 0,-60.4 49.4L575.4,596.7v2.7a256.1,256.1 0,0 0,-43 68.5l-2.5,-1.9c-3.2,-3.2 -5.1,-8.5 -5.8,-15.9l-0.2,-5.9v-47.5h-104.3l-3.6,-0.3a14.5,14.5 0,0 1,-5.7 -2.1,13.5 13.5,0 0,1 -5.2,-7.8 59.4,59.4 0,0 1,-1.7 -16.2c0.3,-9.5 2.9,-15.7 7.7,-18.7a35.1,35.1 0,0 1,15.2 -4.1l7.1,-0.3h90.6v-31.9h-59.1l-8.6,-0.3c-13.4,-1 -23,-4.6 -28.4,-10.8 -5.4,-6.1 -8.6,-15.8 -9.5,-29.1l-0.3,-8.3L418.1,329.1l0.3,-8.3c1,-13.2 4.7,-22.9 10.9,-29.1 7.5,-7.4 19.5,-11.4 36,-12l-1.5,-2.3 -3.2,-4.6 -1.5,-2.3c-5.4,-8.3 -7.7,-15.6 -7.1,-21.7 0.6,-6.1 5.1,-12.2 13.3,-18a31.2,31.2 0,0 1,22.4 -6.5c7.7,0.9 14.3,6.4 20,16.6 3.5,6.1 6.7,12.4 9.8,18.6l4.5,9.8 4.7,10.3h44.8l5.8,-9.4 5.1,-8.8c3.2,-5.7 6.5,-11.9 10,-18.7 5.1,-9.9 10.9,-15.7 17.4,-17.7a27.4,27.4 0,0 1,21.2 2.9zM353.7,246.5l7.2,0.3c11.4,1.2 19.6,5.3 24.7,12.4a55.7,55.7 0,0 1,8.6 28.2l0.4,9.5v55.4l-0.3,8.3c-0.9,13.1 -4.1,22.4 -9.5,28a36.6,36.6 0,0 1,-23 9.5l-8.1,0.4h-33.4l-4.2,0.2c-2.4,0.3 -4,0.8 -4.9,1.6 -0.8,0.8 -1.5,2.4 -1.9,4.5l-0.4,3.8 -3.8,33.7 -0.3,3.1c-0.1,2.8 0.5,4.5 1.7,5.4a12.4,12.4 0,0 0,5.5 1.5l4.5,0.1h37.7l7.2,0.3c4.6,0.3 8.7,0.9 12.3,1.9a25.8,25.8 0,0 1,13.1 7.6c3.3,3.6 5.8,9 7.4,15.7 1.5,6.8 2.6,15.6 2.8,26.3a412.8,412.8 0,0 1,-3.3 75.2c-3.2,20.4 -7.9,36.8 -14.3,49.4 -6.4,12.6 -14.1,21.8 -23.1,27.3a56,56 0,0 1,-29.8 8.3c-12.1,0 -26.4,-4.5 -42.9,-13.4 -11.1,-5.5 -18.8,-11.8 -22.9,-18.7 -4.1,-6.9 -3.3,-15.7 2.4,-26.5a23.3,23.3 0,0 1,16.4 -11.5c7.4,-1.6 15,0.6 22.6,6.4 5.8,4.6 10.9,7.7 15.3,9.2 4.5,1.5 8.3,1.9 11.6,1.2a15.8,15.8 0,0 0,8.6 -5.2,35.5 35.5,0 0,0 6,-10.2 149.8,149.8 0,0 0,5.5 -26.5c1.4,-10.9 2.1,-26.5 2.1,-46.8 0,-8 -0.6,-13 -1.9,-15 -0.8,-1.3 -2.4,-2.2 -4.6,-2.7l-3.9,-0.3h-42.9l-7.6,-0.1a133.3,133.3 0,0 1,-12.9 -1,22.7 22.7,0 0,1 -13.6,-6.2 27,27 0,0 1,-6.7 -14.8,76.4 76.4,0 0,1 -0.3,-18.9l1,-8.3 6.2,-61.8 1.3,-8.7c0.9,-5.4 1.8,-10.1 2.8,-14.1a38,38 0,0 1,6.2 -14.3,21 21,0 0,1 11.2,-7.4 56.4,56.4 0,0 1,11.8 -1.9l7.7,-0.2h33.9l3.4,-0.3a7.4,7.4 0,0 0,4.5 -2.2c1,-1.3 1.7,-3.8 2,-7.6l0.1,-4.2v-20.3l-0.2,-4.7a17.7,17.7 0,0 0,-1.5 -6.4c-0.8,-1.4 -2.2,-2.4 -4.5,-2.8l-3.8,-0.4L280.3,297.2l-6,-0.4a20.6,20.6 0,0 1,-12.6 -5.6C257.9,287.2 256,280.6 256,271.4c0,-9.5 1.9,-16 5.8,-19.6a21.9,21.9 0,0 1,12.5 -5l6,-0.3h73.4zM629.6,419.1L575.4,419.1v46.6h48.1c2.9,0 4.6,-0.3 5.2,-0.9 0.4,-0.4 0.7,-1.3 0.9,-2.4l0.1,-2.2v-41.1zM523.8,419.1h-53.8v41.6l0.1,1.9c0.1,1.1 0.4,1.9 0.9,2.2 0.4,0.4 1.3,0.6 2.6,0.8l2.2,0.1h48.1v-46.6zM624.9,328.6h-49.6v47h54.4L629.7,332.8l-0.3,-1.9c-0.6,-1.5 -2.1,-2.3 -4.5,-2.3zM523.8,328.6h-48.1l-2.2,0.1c-1.3,0.1 -2.1,0.5 -2.6,1a4.8,4.8 0,0 0,-0.8 2.4l-0.1,1.9v41.6h53.8v-47.1z\"\n        android:fillColor=\"#000000\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_danmaku_on.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <group\n      android:scaleX=\"0.9\"\n      android:scaleY=\"0.9\"\n      android:translateX=\"51.2\"\n      android:translateY=\"51.2\">\n    <path\n        android:pathData=\"M768,544a224,224 0,1 1,0 448,224 224,0 0,1 0,-448zM832,96a96,96 0,0 1,95.6 86.8L928,192v256a32,32 0,0 1,-63.5 5.8L864,448L864,192a32,32 0,0 0,-26.2 -31.5L832,160L192,160a32,32 0,0 0,-31.5 26.2L160,192v640a32,32 0,0 0,26.2 31.5L192,864h256a32,32 0,0 1,5.8 63.5L448,928L192,928a96,96 0,0 1,-95.6 -86.8L96,832L96,192a96,96 0,0 1,86.8 -95.6L192,96h640zM768,608a160,160 0,1 0,0 320,160 160,0 0,0 0,-320zM867.5,700.5a32,32 0,0 1,3.6 40.8l-3.6,4.5 -96,96a32,32 0,0 1,-40.8 3.6l-4.5,-3.6 -51.2,-51.2a32,32 0,0 1,40.8 -49l4.5,3.6 28.5,28.6 73.3,-73.4a32,32 0,0 1,45.3 0zM631,228c7.9,4.3 12.9,9.3 15,15.2 2,5.9 0.4,13.6 -5,23.1 -1.3,2.2 -2.4,4.3 -3.6,6.5l-1.9,3.3 -2.2,3.6h1.9l8.6,0.3c13.4,1 22.9,4.7 28.4,11 5.4,6.3 8.6,16.1 9.5,29.6l0.3,8.5v136.6l-0.3,8.4c-0.9,13.2 -4,23.2 -9.5,29.7 -5.4,6.6 -14.9,10.4 -28.4,11.5l-8.6,0.3h-60.1v31.9l62.8,-0.1a257.2,257.2 0,0 0,-60.4 49.4L575.4,596.7v2.7a256.1,256.1 0,0 0,-43 68.5l-2.5,-1.9c-3.2,-3.2 -5.1,-8.5 -5.8,-15.9l-0.2,-5.9v-47.5h-104.3l-3.6,-0.3a14.5,14.5 0,0 1,-5.7 -2.1,13.5 13.5,0 0,1 -5.2,-7.8 59.4,59.4 0,0 1,-1.7 -16.2c0.3,-9.5 2.9,-15.7 7.7,-18.7a35.1,35.1 0,0 1,15.2 -4.1l7.1,-0.3h90.6v-31.9h-59.1l-8.6,-0.3c-13.4,-1 -23,-4.6 -28.4,-10.8 -5.4,-6.1 -8.6,-15.8 -9.5,-29.1l-0.3,-8.3L418.1,329.1l0.3,-8.3c1,-13.2 4.7,-22.9 10.9,-29.1 7.5,-7.4 19.5,-11.4 36,-12l-1.5,-2.3 -3.2,-4.6 -1.5,-2.3c-5.4,-8.3 -7.7,-15.6 -7.1,-21.7 0.6,-6.1 5.1,-12.2 13.3,-18a31.2,31.2 0,0 1,22.4 -6.5c7.7,0.9 14.3,6.4 20,16.6 3.5,6.1 6.7,12.4 9.8,18.6l4.5,9.8 4.7,10.3h44.8l5.8,-9.4 5.1,-8.8c3.2,-5.7 6.5,-11.9 10,-18.7 5.1,-9.9 10.9,-15.7 17.4,-17.7a27.4,27.4 0,0 1,21.2 2.9zM353.7,246.5l7.2,0.3c11.4,1.2 19.6,5.3 24.7,12.4a55.7,55.7 0,0 1,8.6 28.2l0.4,9.5v55.4l-0.3,8.3c-0.9,13.1 -4.1,22.4 -9.5,28a36.6,36.6 0,0 1,-23 9.5l-8.1,0.4h-33.4l-4.2,0.2c-2.4,0.3 -4,0.8 -4.9,1.6 -0.8,0.8 -1.5,2.4 -1.9,4.5l-0.4,3.8 -3.8,33.7 -0.3,3.1c-0.1,2.8 0.5,4.5 1.7,5.4a12.4,12.4 0,0 0,5.5 1.5l4.5,0.1h37.7l7.2,0.3c4.6,0.3 8.7,0.9 12.3,1.9a25.8,25.8 0,0 1,13.1 7.6c3.3,3.6 5.8,9 7.4,15.7 1.5,6.8 2.6,15.6 2.8,26.3a412.8,412.8 0,0 1,-3.3 75.2c-3.2,20.4 -7.9,36.8 -14.3,49.4 -6.4,12.6 -14.1,21.8 -23.1,27.3a56,56 0,0 1,-29.8 8.3c-12.1,0 -26.4,-4.5 -42.9,-13.4 -11.1,-5.5 -18.8,-11.8 -22.9,-18.7 -4.1,-6.9 -3.3,-15.7 2.4,-26.5a23.3,23.3 0,0 1,16.4 -11.5c7.4,-1.6 15,0.6 22.6,6.4 5.8,4.6 10.9,7.7 15.3,9.2 4.5,1.5 8.3,1.9 11.6,1.2a15.8,15.8 0,0 0,8.6 -5.2,35.5 35.5,0 0,0 6,-10.2 149.8,149.8 0,0 0,5.5 -26.5c1.4,-10.9 2.1,-26.5 2.1,-46.8 0,-8 -0.6,-13 -1.9,-15 -0.8,-1.3 -2.4,-2.2 -4.6,-2.7l-3.9,-0.3h-42.9l-7.6,-0.1a133.3,133.3 0,0 1,-12.9 -1,22.7 22.7,0 0,1 -13.6,-6.2 27,27 0,0 1,-6.7 -14.8,76.4 76.4,0 0,1 -0.3,-18.9l1,-8.3 6.2,-61.8 1.3,-8.7c0.9,-5.4 1.8,-10.1 2.8,-14.1a38,38 0,0 1,6.2 -14.3,21 21,0 0 1 11.2,-7.4 56.4,56.4 0,0 1,11.8 -1.9l7.7,-0.2h33.9l3.4,-0.3a7.4,7.4 0,0 0,4.5 -2.2c1,-1.3 1.7,-3.8 2,-7.6l0.1,-4.2v-20.3l-0.2,-4.7a17.7,17.7 0,0 0,-1.5 -6.4c-0.8,-1.4 -2.2,-2.4 -4.5,-2.8l-3.8,-0.4L280.3,297.2l-6,-0.4a20.6,20.6 0,0 1,-12.6 -5.6C257.9,287.2 256,280.6 256,271.4c0,-9.5 1.9,-16 5.8,-19.6a21.9,21.9 0,0 1,12.5 -5l6,-0.3h73.4zM629.6,419.1L575.4,419.1v46.6h48.1c2.9,0 4.6,-0.3 5.2,-0.9 0.4,-0.4 0.7,-1.3 0.9,-2.4l0.1,-2.2v-41.1zM523.8,419.1h-53.8v41.6l0.1,1.9c0.1,1.1 0.4,1.9 0.9,2.2 0.4,0.4 1.3,0.6 2.6,0.8l2.2,0.1h48.1v-46.6zM624.8,328.6h-49.6v47h54.4L629.6,332.8l-0.3,-1.9c-0.6,-1.5 -2.1,-2.3 -4.5,-2.3zM523.8,328.6h-48.1l-2.2,0.1c-1.3,0.1 -2.1,0.5 -2.6,1a4.8,4.8 0,0 0,-0.8 2.4l-0.1,1.9v41.6h53.8v-47.1z\"\n        android:fillColor=\"#000000\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_play_mode_custom.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <group\n            android:scaleX=\"0.9\"\n            android:scaleY=\"0.9\"\n            android:translateX=\"51.2\"\n            android:translateY=\"51.2\">\n    <path\n        android:pathData=\"M734.272 55.552a217.6 217.6 0 0 1 217.6 217.6v308.16a38.4 38.4 0 1 1-76.8 0V273.088a140.8 140.8 0 0 0-140.8-140.8H256.448a140.8 140.8 0 0 0-140.8 140.8v477.824a140.8 140.8 0 0 0 140.8 140.8h405.056a38.4 38.4 0 1 1 0 76.8H256.448a217.6 217.6 0 0 1-217.6-217.6V273.088a217.6 217.6 0 0 1 217.6-217.6h477.824z\"\n        android:fillColor=\"#000000\"/>\n    <path\n        android:pathData=\"M810.88 947.2a217.984 217.984 0 0 0-143.744-143.616c-3.072 0-6.144-3.072-9.152-9.216-3.072-6.08 3.008-12.16 9.152-15.296a217.984 217.984 0 0 0 143.68-143.68c0-3.072 3.072-6.08 9.216-9.152 6.08-3.072 12.16 3.072 15.232 9.152a217.984 217.984 0 0 0 143.744 143.68c3.008 3.072 6.08 6.144 6.08 12.288 0 6.08-3.072 9.152-6.08 12.16a217.984 217.984 0 0 0-143.744 143.744c0 6.08-6.08 9.152-12.16 9.152-6.144 0-12.288-3.072-12.288-9.152z\"\n        android:fillColor=\"#000000\"/>\n    <path\n        android:pathData=\"M731.136 350.912a66.176 66.176 0 0 1 4.864 24.96 39.168 39.168 0 0 1-2.688 14.08 43.328 43.328 0 0 1-8.704 13.632 571.2 571.2 0 0 1-11.648 11.392c-3.456 3.2-6.4 6.144-8.96 8.64a145.216 145.216 0 0 1-8.128 7.616L594.368 329.728l15.488-14.336c5.952-5.632 10.88-10.048 14.912-13.312a47.872 47.872 0 0 1 31.744-10.56 60.288 60.288 0 0 1 15.168 2.432c4.928 1.472 9.152 3.072 12.8 4.864 7.552 4.032 16 10.944 25.216 20.928 9.216 9.92 16.384 20.352 21.44 31.168z m-393.6 230.976l15.168-15.488c7.936-8.128 17.92-18.176 29.824-30.08l39.68-39.68 43.904-43.904L582.784 336l101.44 102.016-116.672 116.672-43.392 43.904c-14.464 14.08-27.52 27.072-39.04 38.848-11.584 11.712-21.184 21.44-28.8 28.992a228.608 228.608 0 0 1-26.048 23.36c-4.672 3.584-9.6 6.528-14.656 8.64-5.056 2.56-12.48 5.632-22.208 9.28a767.872 767.872 0 0 1-61.056 19.52c-9.6 2.56-16.704 4.16-21.44 4.864-9.792 1.088-16.256-0.32-19.52-4.352-3.264-3.968-4.16-10.624-2.752-20.032 0.768-5.12 2.496-12.416 5.184-22.016 2.816-9.984 5.76-19.904 8.704-29.824 3.072-10.304 6.016-19.84 8.96-28.48 2.88-8.704 5.248-14.72 7.04-17.92 2.176-5.12 4.608-9.664 7.296-13.824 2.752-4.16 6.592-8.768 11.648-13.824z\"\n        android:fillColor=\"#000000\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_play_mode_list_order.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <group\n      android:pivotX=\"512\"\n      android:pivotY=\"512\"\n      android:rotation=\"90\">\n    <path\n        android:pathData=\"M102.4 546.133333v341.333334h341.333333V546.133333h-341.333333z m273.066667 273.066667h-204.8V614.4h204.8v204.8zM102.4 136.533333h136.533333v68.266667h-136.533333zM307.2 409.6h136.533333v68.266667h-136.533333zM375.466667 136.533333h68.266666v136.533334h-68.266666zM102.4 341.333333h68.266667v136.533334h-68.266667zM273.066667 136.533333h68.266666v68.266667H273.066667zM102.4 245.76h68.266667v68.266667h-68.266667zM204.8 409.6h68.266667v68.266667H204.8zM375.466667 307.2h68.266666v68.266667h-68.266666zM682.666667 341.333333h68.266666v546.133334h-68.266666zM512 341.333333l204.8-204.8 204.8 204.8z\"\n        android:fillColor=\"#000000\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_play_mode_list_order_reverse.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <group\n      android:pivotX=\"512\"\n      android:pivotY=\"512\"\n      android:rotation=\"90\"\n      android:scaleY=\"-1\">\n    <path\n        android:pathData=\"M102.4 546.133333v341.333334h341.333333V546.133333h-341.333333z m273.066667 273.066667h-204.8V614.4h204.8v204.8zM102.4 136.533333h136.533333v68.266667h-136.533333zM307.2 409.6h136.533333v68.266667h-136.533333zM375.466667 136.533333h68.266666v136.533334h-68.266666zM102.4 341.333333h68.266667v136.533334h-68.266667zM273.066667 136.533333h68.266666v68.266667H273.066667zM102.4 245.76h68.266667v68.266667h-68.266667zM204.8 409.6h68.266667v68.266667H204.8zM375.466667 307.2h68.266666v68.266667h-68.266666zM682.666667 341.333333h68.266666v546.133334h-68.266666zM512 341.333333l204.8-204.8 204.8 204.8z\"\n        android:fillColor=\"#000000\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_play_mode_part_and_episode.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:width=\"24dp\"\n  android:height=\"24dp\"\n  android:viewportWidth=\"1027\"\n  android:viewportHeight=\"1024\">\n  <group\n      android:pivotX=\"513.5\"\n      android:pivotY=\"512\"\n      android:rotation=\"180\"\n      android:scaleX=\"-0.8\"\n      android:scaleY=\"-0.95\">\n    <path\n      android:pathData=\"M35.84 235.52h627.84a30.08 30.08 0 0 0 28.8 -32 30.08 30.08 0 0 0 -28.8 -32H35.84a30.72 30.72 0 0 0 -29.44 32 30.72 30.72 0 0 0 29.44 32zM33.92 535.68h628.48a30.08 30.08 0 0 0 28.8 -32 30.72 30.72 0 0 0 -28.8 -32H33.92a30.72 30.72 0 0 0 -28.8 32 30.08 30.08 0 0 0 28.8 32zM529.28 788.48H32a32 32 0 0 0 0 64h497.28a32 32 0 0 0 0 -64z\"\n      android:fillColor=\"#000000\"\n      android:strokeColor=\"#000000\"\n      android:strokeWidth=\"24\" />\n    <path\n      android:pathData=\"M1017.6 633.6a30.72 30.72 0 0 0 -44.8 0l-110.08 110.72V203.52a32 32 0 1 0 -64 0v540.8L689.28 633.6a32 32 0 0 0 -45.44 0 32.64 32.64 0 0 0 0 45.44l163.84 163.84a30.08 30.08 0 0 0 23.04 9.6 30.72 30.72 0 0 0 23.68 -9.6l163.2 -163.84a32 32 0 0 0 0 -45.44z\"\n      android:fillColor=\"#000000\"\n      android:strokeColor=\"#000000\"\n      android:strokeWidth=\"24\" />\n  </group>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_play_mode_part_and_episode_reverse.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:width=\"24dp\"\n  android:height=\"24dp\"\n  android:viewportWidth=\"1027\"\n  android:viewportHeight=\"1024\">\n  <group\n    android:pivotX=\"513.5\"\n    android:pivotY=\"512\"\n    android:rotation=\"180\"\n    android:scaleX=\"-0.8\"\n    android:scaleY=\"0.95\">\n    <path\n      android:pathData=\"M35.84 235.52h627.84a30.08 30.08 0 0 0 28.8 -32 30.08 30.08 0 0 0 -28.8 -32H35.84a30.72 30.72 0 0 0 -29.44 32 30.72 30.72 0 0 0 29.44 32zM33.92 535.68h628.48a30.08 30.08 0 0 0 28.8 -32 30.72 30.72 0 0 0 -28.8 -32H33.92a30.72 30.72 0 0 0 -28.8 32 30.08 30.08 0 0 0 28.8 32zM529.28 788.48H32a32 32 0 0 0 0 64h497.28a32 32 0 0 0 0 -64z\"\n      android:fillColor=\"#000000\"\n      android:strokeColor=\"#000000\"\n      android:strokeWidth=\"24\" />\n    <path\n      android:pathData=\"M1017.6 633.6a30.72 30.72 0 0 0 -44.8 0l-110.08 110.72V203.52a32 32 0 1 0 -64 0v540.8L689.28 633.6a32 32 0 0 0 -45.44 0 32.64 32.64 0 0 0 0 45.44l163.84 163.84a30.08 30.08 0 0 0 23.04 9.6 30.72 30.72 0 0 0 23.68 -9.6l163.2 -163.84a32 32 0 0 0 0 -45.44z\"\n      android:fillColor=\"#000000\"\n      android:strokeColor=\"#000000\"\n      android:strokeWidth=\"24\" />\n  </group>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_play_mode_related_video.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <group\n      android:scaleX=\"0.9\"\n      android:scaleY=\"0.9\"\n      android:translateX=\"51.2\"\n      android:translateY=\"51.2\">\n    <path\n        android:pathData=\"M247.253333 231.381333c60.288 0 116.864 29.141333 151.893334 78.165334l265.386666 371.584a101.333333 101.333333 0 0 0 82.474667 42.410666h49.706667l-31.744-33.408a42.666667 42.666667 0 0 1-2.048-56.405333l3.626666-3.925333a42.666667 42.666667 0 0 1 60.330667 1.578666l100.053333 105.472a42.666667 42.666667 0 0 1 0 58.752l-100.053333 105.429334a42.666667 42.666667 0 1 1-61.866667-58.709334l31.658667-33.450666h-49.664a186.666667 186.666667 0 0 1-145.877333-70.186667l-6.058667-7.978667-265.386667-371.541333a101.333333 101.333333 0 0 0-82.432-42.453333H128a42.666667 42.666667 0 1 1 0-85.333334h119.253333z m123.392 390.058667a42.666667 42.666667 0 0 1 69.418667 49.578667l-40.96 57.301333a186.666667 186.666667 0 0 1-151.893333 78.165333H128a42.666667 42.666667 0 0 1 0-85.333333h119.253333c32.725333 0 63.445333-15.786667 82.432-42.410667l40.96-57.301333z m395.946667-500.053333a42.666667 42.666667 0 0 1 60.288 1.578666l100.053333 105.429334a42.666667 42.666667 0 0 1 0 58.752L826.88 392.618667a42.666667 42.666667 0 0 1-61.866667-58.752l31.701334-33.450667h-49.706667c-30.208 0-58.709333 13.482667-77.866667 36.522667l-4.608 5.973333L618.666667 407.04a42.666667 42.666667 0 1 1-69.418667-49.621333l45.781333-64.128a186.666667 186.666667 0 0 1 151.893334-78.165334l49.706666-0.042666-31.701333-33.408a42.666667 42.666667 0 0 1-2.048-56.362667l3.626667-3.925333z\"\n        android:fillColor=\"#000000\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_play_mode_single.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"1143\"\n    android:viewportHeight=\"1024\">\n  <group\n      android:scaleX=\"0.8\"\n      android:scaleY=\"0.8\"\n      android:translateX=\"114.3\"\n      android:translateY=\"102.4\">\n    <path\n        android:pathData=\"M673.52381 982.095238c0 22.857143-19.047619 41.904762-41.904762 41.904762H122.666667c-67.809524 0-122.666667-54.857143-122.666667-122.666667V122.666667C0 54.857143 54.857143 0 122.666667 0h778.666666c67.809524 0 122.666667 54.857143 122.666667 122.666667v362.666666c0 22.857143-19.047619 41.904762-41.904762 41.904762s-41.904762-19.047619-41.904762-41.904762V122.666667c0-21.333333-17.52381-38.857143-38.857143-38.857143H122.666667c-21.333333 0-38.857143 17.52381-38.857143 38.857143v778.666666c0 21.333333 17.52381 38.857143 38.857143 38.857143H632.380952c22.857143 0 41.904762 19.047619 41.904762 41.904762z m451.047619-198.857143l-280.380953-161.523809a35.961905 35.961905 0 0 0-48.761905 12.952381c-3.047619 5.333333-4.571429 11.428571-4.571428 17.523809v323.809524c0 19.809524 16 35.809524 35.809524 35.809524 6.095238 0 12.190476-1.52381 18.285714-4.571429l280.380952-161.523809c16.761905-9.904762 22.857143-32 12.952381-48.761905a35.2 35.2 0 0 0-12.952381-12.952381zM556.190476 761.904762V262.095238h-64.761905c-41.904762 32-92.190476 56.380952-150.095238 73.142857v87.619048c18.285714-2.285714 38.095238-7.619048 60.190477-16s38.857143-18.285714 51.809523-29.714286v385.52381H556.190476z\"\n        android:fillColor=\"#000000\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_play_mode_single_loop.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <group\n      android:scaleX=\"1.1\"\n      android:scaleY=\"1.1\"\n      android:translateX=\"-51.2\"\n      android:translateY=\"-51.2\">\n    <path\n        android:pathData=\"M725.333333 725.333333H298.666667v-85.333333l-170.666667 128 170.666667 128v-85.333333h469.333333c25.6 0 42.666667-17.066667 42.666667-42.666667v-213.333333h-85.333334v170.666666zM298.666667 298.666667h426.666666v85.333333l170.666667-128-170.666667-128v85.333333H256c-25.6 0-42.666667 17.066667-42.666667 42.666667v213.333333h85.333334V298.666667z\"\n        android:fillColor=\"#000000\"/>\n    <path\n        android:pathData=\"M554.666667 640V384h-85.333334l-42.666666 42.666667v42.666666h42.666666v170.666667z\"\n        android:fillColor=\"#000000\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_subtitle_off.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n    android:viewportWidth=\"200\"\n    android:viewportHeight=\"200\">\n  <path\n      android:pathData=\"M30.3,26c-1.8,1 -3.9,3.2 -4.7,4.8l-1.5,2.9 -0.3,64.4 -0.3,64.4 1.8,3.3c1,1.8 3.2,4.2 5,5.2l3.2,2 33.5,-0.2 33.4,-0.3 1.5,-2.6 1.5,-2.7 -1.2,-4.1 -1.3,-4.1 -31.4,-0 -31.5,-0 0,-60.5 0,-60.5 62.5,-0 62.6,-0 -0.3,33.5 -0.3,33.4 3.7,2.1 3.6,2 2.8,-1.1c1.6,-0.5 3.2,-1.6 3.6,-2.2 0.4,-0.7 0.8,-17.1 0.8,-36.5l0,-35.3 -1.5,-3c-0.9,-1.6 -3,-3.9 -4.8,-4.9l-3.2,-2 -67,-0 -67,-0 -3.2,2z\"\n      android:fillColor=\"#0A0A0A\"\n      android:strokeColor=\"#00000000\"/>\n  <path\n      android:pathData=\"M51.2,70.2l-1.2,1.2 0,26.6 0,26.6 1.2,1.2 1.2,1.2 19.6,-0 19.6,-0 1.2,-1.2 1.2,-1.2 0,-9.3 0,-9.3 -6.5,-0 -6.5,-0 0,3.5 0,3.5 -9,-0 -9,-0 0,-15.5 0,-15.5 9,-0 9,-0 0,4 0,4 6.5,-0 6.5,-0 0,-9.3 0,-9.3 -1.2,-1.2 -1.2,-1.2 -19.6,-0 -19.6,-0 -1.2,1.2z\"\n      android:fillColor=\"#0A0A0A\"\n      android:strokeColor=\"#00000000\"/>\n  <path\n      android:pathData=\"M107.2,70.2l-1.2,1.2 0,23.1 0,23.2 6.5,-6.5 6.5,-6.5 0,-11.4 0,-11.3 9,-0 9,-0 0,4 0,4 6.5,-0 6.5,-0 0,-9.3 0,-9.3 -1.2,-1.2 -1.2,-1.2 -19.6,-0 -19.6,-0 -1.2,1.2z\"\n      android:fillColor=\"#0A0A0A\"\n      android:strokeColor=\"#00000000\"/>\n  <path\n      android:pathData=\"M137.9,108c-15,2.6 -26.4,13 -31.1,28.4l-2.1,6.8 0.7,7 0.6,6.9 3,6.5 2.9,6.4 5.3,5.2c22.3,21.7 58.6,11.2 66.4,-19.2l1.6,-6.5 -0.6,-5.7c-1.6,-15.4 -12.2,-29.3 -26.1,-34.1 -5.4,-1.9 -15,-2.7 -20.6,-1.7zM157.1,122.3c4.6,2 9.5,7.1 12.7,13.2l2.6,5 -0.1,6.5c-0.1,6.1 -2.4,14 -4.1,14 -0.9,-0 -37.2,-36.2 -37.2,-37.2 0,-0.8 7.6,-3.6 11,-4 3.6,-0.5 11.1,0.8 15.1,2.5zM140.7,151.2c10.1,10.1 18.3,18.6 18.3,19.1 0,1.7 -7.8,4 -14,4l-6.5,0.1 -4.8,-2.2c-5.8,-2.6 -11.3,-8.2 -14,-14.2l-2,-4.5 -0.1,-6 -0.1,-6.1 1.9,-4.2c1,-2.3 2.2,-4.2 2.5,-4.2 0.3,-0 8.8,8.2 18.8,18.2z\"\n      android:fillColor=\"#0A0A0A\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/ic_subtitle_on.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n    android:viewportWidth=\"200\"\n    android:viewportHeight=\"200\">\n  <path\n      android:pathData=\"M30.3,26c-1.8,1 -3.9,3.2 -4.7,4.8l-1.5,2.9 -0.3,64.4 -0.3,64.4 1.8,3.3c1,1.8 3.2,4.2 5,5.2l3.2,2 33.5,-0.2 33.4,-0.3 1.5,-2.6 1.5,-2.7 -1.2,-4.1 -1.3,-4.1 -31.4,-0 -31.5,-0 0,-60.5 0,-60.5 62.5,-0 62.6,-0 -0.3,33.5 -0.3,33.4 3.7,2.1 3.6,2 2.8,-1.1c1.6,-0.5 3.2,-1.6 3.6,-2.2 0.4,-0.7 0.8,-17.1 0.8,-36.5l0,-35.3 -1.5,-3c-0.9,-1.6 -3,-3.9 -4.8,-4.9l-3.2,-2 -67,-0 -67,-0 -3.2,2z\"\n      android:fillColor=\"#0A0A0A\"\n      android:strokeColor=\"#00000000\"/>\n  <path\n      android:pathData=\"M51.2,70.2l-1.2,1.2 0,26.6 0,26.6 1.2,1.2 1.2,1.2 19.6,-0 19.6,-0 1.2,-1.2 1.2,-1.2 0,-9.3 0,-9.3 -6.5,-0 -6.5,-0 0,3.5 0,3.5 -9,-0 -9,-0 0,-15.5 0,-15.5 9,-0 9,-0 0,4 0,4 6.5,-0 6.5,-0 0,-9.3 0,-9.3 -1.2,-1.2 -1.2,-1.2 -19.6,-0 -19.6,-0 -1.2,1.2z\"\n      android:fillColor=\"#0A0A0A\"\n      android:strokeColor=\"#00000000\"/>\n  <path\n      android:pathData=\"M107.2,70.2l-1.2,1.2 0,23.1 0,23.2 6.5,-6.5 6.5,-6.5 0,-11.4 0,-11.3 9,-0 9,-0 0,4 0,4 6.5,-0 6.5,-0 0,-9.3 0,-9.3 -1.2,-1.2 -1.2,-1.2 -19.6,-0 -19.6,-0 -1.2,1.2z\"\n      android:fillColor=\"#0A0A0A\"\n      android:strokeColor=\"#00000000\"/>\n  <path\n      android:pathData=\"M137.8,108c-16.3,2.9 -28.8,15.7 -32,33l-1.2,6 1.2,6.2c0.6,3.5 2.6,9.1 4.3,12.6l3.1,6.3 5.8,4.8c3.2,2.7 8.5,5.9 11.7,7.2l5.8,2.2 8.5,-0 8.5,-0 5.7,-2.2c12.2,-4.7 21,-14.9 24.4,-28.2l1.6,-6.4 -0.6,-5.7c-1.6,-15.4 -12.2,-29.3 -26.1,-34.1 -5.5,-1.9 -15,-2.7 -20.7,-1.7zM157.2,122.4c5.2,2.5 10.2,7.6 12.9,13.3l2.3,4.8 0,6.5c-0.1,8.1 -1.9,12.5 -7.9,18.9l-4.7,5 -4.6,1.8c-6,2.3 -14.4,2.3 -20.4,-0l-4.6,-1.8 -4.7,-5c-9.7,-10.4 -10.9,-22.2 -3.3,-34.1 5.2,-8.2 12.8,-11.9 23.8,-11.5l7.5,0.3 3.7,1.8z\"\n      android:fillColor=\"#0A0A0A\"\n      android:strokeColor=\"#00000000\"/>\n  <path\n      android:pathData=\"M148.6,139.9l-7.1,6.9 -3.3,-2.9c-1.8,-1.6 -4.3,-2.9 -5.6,-2.9 -3,-0 -5.6,3 -5.6,6.5l0,2.8 5.8,5.9 5.9,5.8 2.8,-0 2.8,-0 9.4,-9.3c5.2,-5 9.9,-10.2 10.4,-11.5l0.9,-2.3 -1.1,-2.2c-1.5,-2.8 -2.8,-3.7 -5.8,-3.7l-2.3,-0 -7.2,6.9z\"\n      android:fillColor=\"#0A0A0A\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/next_play_fill.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M810.7,128m42.7,0l42.7,0q42.7,0 42.7,42.7l0,682.7q0,42.7 -42.7,42.7l-42.7,0q-42.7,0 -42.7,-42.7l0,-682.7q0,-42.7 42.7,-42.7Z\"/>\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M655.8,460.5L180.7,138.5C140.2,110.9 85.3,140.9 85.3,190.3v643.4c0,50.2 54.8,79.3 95.4,51.8l475.1,-322c35.8,-23.5 35.8,-78.5 0,-102.8z\"/>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/person.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M512,0c165.4,0 299.5,135.4 299.5,302.6a303,303 0,0 1,-126.9 247.2C848.9,623.1 972.8,793.8 972.8,977.4 972.8,1003.2 952.1,1024 926.7,1024c-25.5,0 -46.1,-20.8 -46.1,-46.6 0,-192.8 -177.8,-372.4 -368.6,-372.4 -190.9,0 -368.6,179.6 -368.6,372.4C143.4,1003.2 122.7,1024 97.3,1024 71.8,1024 51.2,1003.2 51.2,977.4c0,-183.6 123.9,-354.3 288.2,-427.7a302.9,302.9 0,0 1,-126.9 -247.2C212.5,135.4 346.6,0 512,0zM512,93.1c-114.5,0 -207.4,93.8 -207.4,209.5 0,115.7 92.8,209.4 207.4,209.4s207.4,-93.8 207.4,-209.4c0,-115.7 -92.8,-209.5 -207.4,-209.5z\"/>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/drawable/person_following.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M747.8,736.6c8,9.9 19.8,16.6 33.6,16.6 24.2,0 44,-19.7 44,-44.1L825.2,707.2c0,0 1.6,-12.1 -3.4,-18.3C818.3,680.5 812.2,673.9 804.8,669 763.8,624.9 714.4,589.2 658.4,565 739.5,509.3 792.8,415.8 792.8,309.7c0,-170.8 -138,-309.2 -308.2,-309.2S176.4,138.9 176.4,309.7c0,106.1 53.3,199.7 134.5,255.3 -136.9,59.1 -236.1,186.5 -259.3,339 -3.2,6.1 -5.4,12.7 -5.4,20.1l0.4,4.1c0,24.3 19.7,44.1 44,44.1 24.2,0 44,-19.7 44,-44.1l-0.1,-0.5 1.1,0C156.4,756.9 298.7,623.9 473.3,618.4 477.1,618.5 480.8,619 484.6,619c3.8,0 7.5,-0.4 11.3,-0.6 100.4,3.2 189.8,48.6 251.9,118.9L747.8,736.6zM484.5,530.6c-121.6,0 -220.2,-98.9 -220.2,-220.9 0,-122 98.6,-220.9 220.2,-220.9s220.2,98.9 220.2,220.9C704.7,431.8 606.1,530.6 484.5,530.6zM1000.9,766.1c-17.2,-17.2 -45,-17.2 -62.1,0l-146.2,145.9L734.4,854.3c-17.2,-17.2 -45,-17.2 -62.1,0 -17.2,17.2 -17.2,45.1 0,62.4l84.6,84.1c1.4,1.8 2,3.9 3.7,5.6 8.8,8.9 20.5,13 32,12.7 11.6,0.3 23.2,-3.8 32,-12.7 1.7,-1.7 2.4,-3.8 3.8,-5.7l172.5,-172.2C1018,811.2 1018,783.3 1000.9,766.1z\"/>\n</vector>\n"
  },
  {
    "path": "player/shared/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"audio_132k\">132K</string>\n    <string name=\"audio_192k\">192K</string>\n    <string name=\"audio_64k\">64K</string>\n    <string name=\"audio_dolby_atoms\">Dolby Atoms</string>\n    <string name=\"audio_hi_res\">Hi-Res</string>\n\n    <string name=\"play_mode_single_video\">单视频</string>\n    <string name=\"play_mode_single_loop\">单视频循环</string>\n    <string name=\"play_mode_part_episode\">合集/分P</string>\n    <string name=\"play_mode_part_episode_reverse\">合集/分P-逆序</string>\n    <string name=\"play_mode_list_order\">UGC列表</string>\n    <string name=\"play_mode_list_order_reverse\">UGC列表-逆序</string>\n    <string name=\"play_mode_related_video\">UGC推荐-随机</string>\n    <string name=\"play_mode_custom\">自定义</string>\n\n    <string name=\"resolution_1080p\">1080P 高清</string>\n    <string name=\"resolution_1080p_60\">1080P60 高帧率</string>\n    <string name=\"resolution_1080p_60_short\">1080P60</string>\n    <string name=\"resolution_1080p_plus\">1080P+ 高码率</string>\n    <string name=\"resolution_1080p_plus_short\">1080P+</string>\n    <string name=\"resolution_1080p_short\">1080P</string>\n    <string name=\"resolution_240p\">240P 极速</string>\n    <string name=\"resolution_240p_short\">240P</string>\n    <string name=\"resolution_360p\">360P 流畅</string>\n    <string name=\"resolution_360p_short\">360P</string>\n    <string name=\"resolution_480p\">480P 清晰</string>\n    <string name=\"resolution_480p_short\">480P</string>\n    <string name=\"resolution_4k\">4K 超清</string>\n    <string name=\"resolution_4k_short\">4K</string>\n    <string name=\"resolution_720p\">720P 高清</string>\n    <string name=\"resolution_720p_60_short\">720P60</string>\n    <string name=\"resolution_720p_short\">720P</string>\n    <string name=\"resolution_8k\">8K 超高清</string>\n    <string name=\"resolution_8k_short\">8K</string>\n    <string name=\"resolution__720p_60\">720P60 高帧率</string>\n    <string name=\"resolution_dolby_bision_short\">杜比视界</string>\n    <string name=\"resolution_dolby_vision\">杜比视界</string>\n    <string name=\"resolution_hdr\">HDR 真彩色</string>\n    <string name=\"resolution_hdr_short\">HDR</string>\n\n    <string name=\"video_aspect_ratio_default\">默认</string>\n    <string name=\"video_aspect_ratio_four_to_three\">4:3</string>\n    <string name=\"video_aspect_ratio_sixteen_to_nine\">16:9</string>\n    <string name=\"video_aspect_ratio_nine_to_sixteen\">9:16</string>\n    <string name=\"video_aspect_ratio_equal_width\">等宽</string>\n    <string name=\"video_aspect_ratio_equal_height\">等高</string>\n    <string name=\"video_aspect_ratio_stretch\">拉伸</string>\n    <string name=\"video_codec_av1\" tools:ignore=\"Typos\">AV1</string>\n    <string name=\"video_codec_avc\">H.264</string>\n    <string name=\"video_codec_dvh1\" tools:ignore=\"Typos\">Dolby Vision (DVH1)</string>\n    <string name=\"video_codec_hevc\">H.265</string>\n    <string name=\"video_codec_hvc1\" tools:ignore=\"Typos\">Dolby Vision (HVC1)</string>\n    <!-- 直播编码格式选项 -->\n    <string name=\"live_codec_hls\">HLS（自动）</string>\n    <string name=\"live_codec_flv\">FLV</string>\n    <string name=\"live_codec_avc\">HLS（AVC）</string>\n    <string name=\"video_player_menu_danmaku_rolling_duration_factor\">弹幕速度</string>\n    <string name=\"video_player_menu_danmaku_area\">显示区域</string>\n    <string name=\"video_player_menu_danmaku_mask\">人像防挡</string>\n    <string name=\"video_player_menu_danmaku_opacity\">不透明度</string>\n    <string name=\"video_player_menu_danmaku_size\">字体缩放</string>\n    <string name=\"video_player_menu_danmaku_switch\">弹幕类型</string>\n    <string name=\"video_player_menu_danmaku_type_all\">全部弹幕</string>\n    <string name=\"video_player_menu_danmaku_type_bottom\">底部弹幕</string>\n    <string name=\"video_player_menu_danmaku_type_cross\">滚动弹幕</string>\n    <string name=\"video_player_menu_danmaku_type_top\">顶部弹幕</string>\n    <string name=\"video_player_menu_danmaku_filter_level\">过滤弹幕等级</string>\n    <string name=\"video_player_menu_danmaku_filter_user_level\">过滤用户等级</string>\n    <string name=\"video_player_menu_nav_danmaku\">弹幕设置</string>\n    <string name=\"video_player_menu_nav_others\">其它设置</string>\n    <string name=\"video_player_menu_nav_picture\">画面音频</string>\n    <string name=\"video_player_menu_nav_subtitle\">字幕设置</string>\n    <string name=\"video_player_menu_others_play_mode\">播放模式</string>\n    <string name=\"video_player_menu_others_debug_info\">调试信息</string>\n    <string name=\"video_player_menu_picture_aspect_ratio\">画面比例</string>\n    <string name=\"video_player_menu_picture_audio\">音频编码</string>\n    <string name=\"video_player_menu_picture_codec\">视频编码</string>\n    <string name=\"video_player_menu_picture_play_speed\">播放速度</string>\n    <string name=\"video_player_menu_picture_resolution\">清晰度</string>\n    <string name=\"video_player_menu_picture_rotation\">画面旋转</string>\n    <string name=\"video_rotation_original\">原始</string>\n    <string name=\"video_rotation_90\">+90° &#x21BB;</string>\n    <string name=\"video_rotation_negative_90\">-90° &#x21BA;</string>\n    <string name=\"video_rotation_180\">+180° &#x21C5;</string>\n    <string name=\"video_player_menu_subtitle_background_opacity\">背景不透明度</string>\n    <string name=\"video_player_menu_subtitle_bottom_padding\">底部间距</string>\n    <string name=\"video_player_menu_subtitle_size\">字体大小</string>\n    <string name=\"video_player_menu_subtitle_switch\">选择字幕</string>\n    <string name=\"video_player_press_back_again_to_exit\">再次按下返回键退出播放</string>\n\n    <string name=\"pvf_mode_none\">不做处理</string>\n    <string name=\"pvf_mode_limit_1080p\">自动限制到 ≤1080P</string>\n    <string name=\"pvf_mode_use_texture_view\">使用 TextureView 渲染</string>\n</resources>\n"
  },
  {
    "path": "player/tv/.gitignore",
    "content": "/build"
  },
  {
    "path": "player/tv/build.gradle.kts",
    "content": "plugins {\n    alias(gradleLibs.plugins.android.library)\n    alias(gradleLibs.plugins.compose.compiler)\n    alias(gradleLibs.plugins.kotlin.android)\n}\n\nandroid {\n    namespace = \"${AppConfiguration.appId}.player.tv\"\n    compileSdk = AppConfiguration.compileSdk\n\n    defaultConfig {\n        minSdk = AppConfiguration.minSdk\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles(\"consumer-rules.pro\")\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"r8Test\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"alpha\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n\n    buildFeatures {\n        compose = true\n    }\n\n    testOptions {\n        targetSdk = AppConfiguration.targetSdk\n    }\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\n\ndependencies {\n    implementation(project(\":player:core\"))\n    implementation(project(\":player:shared\"))\n    implementation(androidx.activity.compose)\n    implementation(androidx.compose.constraintlayout)\n    implementation(androidx.compose.material.icons)\n    implementation(androidx.compose.material3)\n    implementation(androidx.compose.tv.foundation)\n    implementation(androidx.compose.tv.material)\n    implementation(androidx.compose.ui)\n    implementation(androidx.compose.ui.tooling.preview)\n    implementation(androidx.compose.ui.util)\n    implementation(androidx.core.ktx)\n    implementation(libs.coil.compose)\n    implementation(libs.logging)\n    implementation(libs.material)\n    implementation(libs.qrcode)\n    debugImplementation(androidx.compose.ui.test.manifest)\n    debugImplementation(androidx.compose.ui.tooling)\n}"
  },
  {
    "path": "player/tv/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "player/tv/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "player/tv/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/BvPlayer.kt",
    "content": "package dev.aaa1115910.bv.player.tv\n\nimport android.os.CountDownTimer\nimport android.os.SystemClock\nimport dev.aaa1115910.bv.player.tv.controller.EmptyUserActionContent\nimport dev.aaa1115910.bv.player.tv.controller.UserActionContent\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.player.danmaku.DanmakuConfig\nimport dev.aaa1115910.bv.player.danmaku.DanmakuView\nimport dev.aaa1115910.biliapi.entity.danmaku.DanmakuMaskFrame\nimport dev.aaa1115910.biliapi.http.entity.video.ClipType\nimport dev.aaa1115910.biliapi.entity.video.Subtitle\nimport dev.aaa1115910.bv.player.AbstractVideoPlayer\nimport dev.aaa1115910.bv.player.BvVideoPlayer\nimport dev.aaa1115910.bv.player.VideoPlayerListener\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.DanmakuType\nimport dev.aaa1115910.bv.player.entity.LiveCodec\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerClockState\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerDanmakuMasksData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerDebugInfoData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerHistoryData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerLoadStateData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerLogsData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerPaymentData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekState\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerStateData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerVideoInfoData\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.RequestState\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoAspectRatio\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.player.entity.VideoListItem\nimport dev.aaa1115910.bv.player.entity.VideoRotation\nimport dev.aaa1115910.bv.player.entity.VideoPlayerClockState\nimport dev.aaa1115910.bv.player.entity.VideoPlayerDebugInfoData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerSeekState\nimport dev.aaa1115910.bv.player.entity.VideoPlayerStateData\nimport dev.aaa1115910.bv.player.entity.DefaultStartPosition\nimport dev.aaa1115910.bv.player.tv.controller.SkipEdTip\nimport dev.aaa1115910.bv.player.tv.controller.SkipOpTip\nimport dev.aaa1115910.bv.player.tv.controller.VideoPlayerController\nimport dev.aaa1115910.bv.util.countDownTimer\nimport dev.aaa1115910.bv.player.util.DanmakuMaskFinder\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport dev.aaa1115910.bv.util.ifElse\nimport dev.aaa1115910.bv.util.requestFocus\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.util.Calendar\nimport kotlin.math.max\n\nprivate const val HEARTBEAT_MIN_INTERVAL_MS = 14_000L\n\n@Composable\nfun BvPlayer(\n    modifier: Modifier = Modifier,\n    videoPlayer: AbstractVideoPlayer,\n    playerSeekForwardStep: Int = 10,\n    playerSeekBackwardStep: Int = 5,\n    showBottomProgressBar: Boolean = false,\n    useTextureViewFixPortraitVideo: Boolean = false,\n    onSendHeartbeat: suspend (Int) -> Unit,\n    onClearBackToHistoryData: () -> Unit,\n    onLoadNextVideo: (Boolean) -> Unit,\n    onExit: () -> Unit,\n    onLoadNewVideo: (VideoListItem) -> Unit,\n    onResolutionChange: (Resolution, afterChange: suspend () -> Unit) -> Unit,\n    onCodecChange: (VideoCodec, afterChange: suspend () -> Unit) -> Unit,\n    onAspectRatioChange: (VideoAspectRatio) -> Unit,\n    onRotationChange: (VideoRotation) -> Unit,\n    onPlaySpeedChange: (Float) -> Unit,\n    onAudioChange: (Audio, afterChange: suspend () -> Unit) -> Unit,\n    onLiveQualityChange: (Int) -> Unit = {},\n    onLiveCodecChange: (LiveCodec) -> Unit = {},\n    onDanmakuSwitchChange: (List<DanmakuType>) -> Unit,\n    onDanmakuSizeChange: (Float) -> Unit,\n    onDanmakuOpacityChange: (Float) -> Unit,\n    onDanmakuAreaChange: (Float) -> Unit,\n    onDanmakuMaskChange: (Boolean) -> Unit,\n    onDanmakuRollingDurationFactorChange: (Float) -> Unit,\n    onDanmakuFilterLevelChange: (Int) -> Unit = {},\n    onSubtitleChange: (Subtitle) -> Unit,\n    onSubtitleSizeChange: (TextUnit) -> Unit,\n    onSubtitleBackgroundOpacityChange: (Float) -> Unit,\n    onSubtitleBottomPadding: (Dp) -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit,\n    onDebugInfoChange: (Boolean) -> Unit = {},\n    onToggleRelatedVideos: (Boolean) -> Unit = {},\n    onOpenUpSpace: () -> Unit = {},\n    onShowDanmakuChange: (Boolean) -> Unit = {},\n    onRefreshVideo: () -> Unit = {},\n    onLiveRetry: () -> Unit = {},\n    onShowComment: () -> Unit = {},\n    onShowDescription: () -> Unit = {},\n    userActionContent: UserActionContent = EmptyUserActionContent,\n    onViewerCountTipCanShowChanged: (Boolean) -> Unit = {},\n    viewerCountText: String = \"\",\n    danmakuView: DanmakuView,\n) {\n//    // 调试重组次数: AtomicInteger，不被 Compose 追踪，只记录真实由外部状态引起的重组次数。\n//    val recomposeCounter = remember { java.util.concurrent.atomic.AtomicInteger(0) }\n//    SideEffect {\n//        val value = recomposeCounter.incrementAndGet()\n//        println(\"Recompose(BvPlayer): $value\")\n//    }\n\n    val scope = rememberCoroutineScope()\n    val logger = KotlinLogging.logger(\"BvPlayer\")\n    //val tvVideoPlayerData = LocalTvVideoPlayerData.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val currentConfigData by rememberUpdatedState(videoPlayerConfigData)\n    val videoPlayerDanmakuMaskData = LocalVideoPlayerDanmakuMasksData.current\n    val videoPlayerHistoryData = LocalVideoPlayerHistoryData.current\n    val videoPlayerLoadStateData = LocalVideoPlayerLoadStateData.current\n    val videoPlayerLogsData = LocalVideoPlayerLogsData.current\n    val videoPlayerPaymentData = LocalVideoPlayerPaymentData.current\n    val videoPlayerVideoInfoData = LocalVideoPlayerVideoInfoData.current\n\n    val focusRequester = remember { FocusRequester() }\n\n    var showLogs by remember { mutableStateOf(false) }\n    var showBackToHistory by remember { mutableStateOf(false) }\n    var isPlaying by rememberSaveable { mutableStateOf(false) }\n    var isError by remember { mutableStateOf(false) }\n    var isBuffering by remember { mutableStateOf(false) }\n    var exception by remember { mutableStateOf<Exception?>(null) }\n    //var proxyArea by remember { mutableStateOf(ProxyArea.MainLand) }\n\n    var danmakuConfig by remember { mutableStateOf(DanmakuConfig()) }\n\n    val seekState = remember { VideoPlayerSeekState() }\n    var currentVideoAspectRatio by remember { mutableStateOf(videoPlayerConfigData.currentVideoAspectRatio) }\n    var currentVideoRotation by remember { mutableStateOf(videoPlayerConfigData.currentVideoRotation) }\n    var currentPlaySpeed by remember { mutableFloatStateOf(videoPlayerConfigData.currentVideoSpeed) }\n    var aspectRatioValue by remember { mutableFloatStateOf(16f / 9f) }\n    var lastPlayed by remember { mutableLongStateOf(0L) }\n    var defaultAspectRatio by remember { mutableFloatStateOf(16 / 9f) }\n    var showInfoProvider: () -> Boolean by remember { mutableStateOf({ false }) }\n    val lastHeartbeatReportAtMs = remember { java.util.concurrent.atomic.AtomicLong(0L) }\n    val lastHeartbeatTime = remember { java.util.concurrent.atomic.AtomicInteger(Int.MIN_VALUE) }\n\n    val clockState = remember { VideoPlayerClockState() }\n\n    var clockRefreshTimer: CountDownTimer? by remember { mutableStateOf(null) }\n    var hideBackToHistoryTimer: CountDownTimer? by remember { mutableStateOf(null) }\n\n    var currentDanmakuMaskFrame: DanmakuMaskFrame? by remember { mutableStateOf(null) }\n\n    // 跳过片头片尾相关状态\n    var showSkipOpTip by remember { mutableStateOf(false) }\n    var showSkipEdTip by remember { mutableStateOf(false) }\n    var skipOpTipText by remember { mutableStateOf(\"即将跳过片头\") }\n    var skipEdTipText by remember { mutableStateOf(\"即将跳过片尾\") }\n    var processedClipIndices by remember { mutableStateOf(setOf<Int>()) }\n\n    // 使用 rememberUpdatedState 来跟踪 clipInfoList 和 skipPgcIntroOutro 的最新值\n    // 这样可以在非 Composable 上下文（定时器回调）中读取到最新值\n    val currentClipInfoList by rememberUpdatedState(videoPlayerConfigData.clipInfoList)\n    val currentSkipPgcIntroOutro by rememberUpdatedState(videoPlayerConfigData.skipPgcIntroOutro)\n\n    // 当 clipInfoList 变化时，重置已处理的 clip 索引\n    // 这确保了切换到新视频时，跳过片头/片尾功能能够正常工作\n    LaunchedEffect(videoPlayerConfigData.clipInfoList) {\n        processedClipIndices = emptySet()\n    }\n\n    // 跳过片头片尾检测任务\n    val checkSkipTask: (Long) -> Unit = { positionMs ->\n        // 使用 rememberUpdatedState 获取最新值\n        if (currentSkipPgcIntroOutro && currentClipInfoList.isNotEmpty() && isPlaying) {\n            val currentPosition = (positionMs / 1000).toInt()  // 毫秒转秒\n            currentClipInfoList.forEachIndexed { index, clipInfo ->\n                // 跳过已处理的 clip\n                if (index in processedClipIndices) return@forEachIndexed\n\n                when (clipInfo.clipType) {\n                    ClipType.CLIP_TYPE_OP -> {\n                        // 检测是否到达片头开始时间\n                        val inRange = currentPosition >= clipInfo.start && currentPosition < clipInfo.end\n                        if (inRange) {\n                            scope.launch(Dispatchers.Main) {\n                                skipOpTipText = clipInfo.toastText.ifBlank { \"即将跳过片头\" }\n                                showSkipOpTip = true\n                                // 显示提示后短暂延迟再跳转\n                                delay(1500)\n                                videoPlayer.seekTo(clipInfo.end * 1000L)\n                                danmakuView.notifySeek(clipInfo.end * 1000L)\n                                videoPlayer.start()\n                                showSkipOpTip = false\n                            }\n                            processedClipIndices = processedClipIndices + index\n                        }\n                    }\n                    ClipType.CLIP_TYPE_ED -> {\n                        // 检测是否到达片尾开始时间\n                        val inRange = currentPosition >= clipInfo.start && currentPosition < clipInfo.end\n                        if (inRange) {\n                            scope.launch(Dispatchers.Main) {\n                                skipEdTipText = clipInfo.toastText.ifBlank { \"即将跳过片尾\" }\n                                showSkipEdTip = true\n                                delay(1500)\n                                videoPlayer.seekTo(clipInfo.end * 1000L)\n                                danmakuView.notifySeek(clipInfo.end * 1000L)\n                                videoPlayer.start()\n                                showSkipEdTip = false\n                            }\n                            processedClipIndices = processedClipIndices + index\n                        }\n                    }\n                    else -> {}  // 忽略其他类型\n                }\n            }\n        }\n    }\n\n\n    val applyDanmakuConfig: (DanmakuConfig) -> Unit = { newConfig ->\n        danmakuConfig = newConfig\n        danmakuView.setConfig(newConfig)\n        logger.info { \"Update danmaku config: $newConfig\" }\n    }\n\n    val syncDanmakuConfig: () -> Unit = {\n        val danmakuTypes = videoPlayerConfigData.currentDanmakuEnabledList\n        val allowAll = danmakuTypes.contains(DanmakuType.All)\n        val filterLevel = if (videoPlayerConfigData.isLive) videoPlayerConfigData.currentLiveDanmakuFilterLevel else videoPlayerConfigData.currentDanmakuFilterLevel\n        val factor = videoPlayerConfigData.currentDanmakuRollingDurationFactor\n        val durationMultiplier = 2f - factor\n        applyDanmakuConfig(danmakuConfig.copy(\n            enabled = videoPlayerConfigData.showDanmaku,\n            textSizeScale = (videoPlayerConfigData.currentDanmakuScale * 100).toInt(),\n            allowScroll = allowAll || danmakuTypes.contains(DanmakuType.Rolling),\n            allowTop = allowAll || danmakuTypes.contains(DanmakuType.Top),\n            allowBottom = allowAll || danmakuTypes.contains(DanmakuType.Bottom),\n            minLevel = filterLevel,\n            durationMultiplier = durationMultiplier,\n            opacity = currentConfigData.currentDanmakuOpacity,\n            area = currentConfigData.currentDanmakuArea,\n        ))\n    }\n\n    val updateDanmakuConfigTypeFilter: () -> Unit = {\n        val danmakuTypes = videoPlayerConfigData.currentDanmakuEnabledList\n        val allowAll = danmakuTypes.contains(DanmakuType.All)\n        applyDanmakuConfig(danmakuConfig.copy(\n            allowScroll = allowAll || danmakuTypes.contains(DanmakuType.Rolling),\n            allowTop = allowAll || danmakuTypes.contains(DanmakuType.Top),\n            allowBottom = allowAll || danmakuTypes.contains(DanmakuType.Bottom),\n        ))\n    }\n\n    val updateVideoAspectRatio: () -> Unit = {\n        aspectRatioValue = currentVideoAspectRatio.resolveAspectRatio(defaultAspectRatio)\n        logger.info {\n            \"Update video player aspectRatio: type=$currentVideoAspectRatio, ratio=$aspectRatioValue\"\n        }\n    }\n\n    val sendHeartbeat: (CoroutineScope, Boolean) -> Unit = heartbeat@{ launchScope, fromPlaybackEnd ->\n        // 在主线程直接读取播放器状态，避免 IO→Main→IO 双重分发\n        val currentTime = (videoPlayer.currentPosition.coerceAtLeast(0L) / 1000).toInt()\n        val totalTime = (videoPlayer.duration.coerceAtLeast(0L) / 1000).toInt()\n\n        val time = if (totalTime == 0) {\n            -2 // 无法正常播放\n        } else if (currentTime >= totalTime - 1) {\n            if (videoPlayerPaymentData.needPay) {\n                currentTime // 试看结束不能按完整播放上报 -1\n            } else {\n                -1 // 播放完后上报的时间应为 -1\n            }\n        } else {\n            currentTime // 播放中上报当前时间\n        }\n        if (time <= -2) return@heartbeat\n\n        val nowElapsedMs = SystemClock.elapsedRealtime()\n        val previousHeartbeatAtMs = lastHeartbeatReportAtMs.get()\n        val previousHeartbeatTime = lastHeartbeatTime.get()\n        val hasReachedMinInterval =\n            previousHeartbeatAtMs == 0L || nowElapsedMs - previousHeartbeatAtMs >= HEARTBEAT_MIN_INTERVAL_MS\n        val hasProgressChanged = previousHeartbeatTime != time\n        val canReport = if (fromPlaybackEnd) {\n            hasProgressChanged\n        } else {\n            hasReachedMinInterval && hasProgressChanged\n        }\n\n        if (!canReport) {\n            logger.debug {\n                \"Skip heartbeat: time=$time, lastTime=$previousHeartbeatTime, elapsed=${nowElapsedMs - previousHeartbeatAtMs}ms, fromPlaybackEnd=$fromPlaybackEnd\"\n            }\n            return@heartbeat\n        }\n\n        lastHeartbeatReportAtMs.set(nowElapsedMs)\n        lastHeartbeatTime.set(time)\n        launchScope.launch(Dispatchers.IO) {\n            onSendHeartbeat(time)\n        }\n    }\n\n    // updateBackToHistory() 中使用 videoPlayerHistoryData.lastPlayed 无法获取到新值\n    LaunchedEffect(videoPlayerHistoryData.lastPlayed) {\n        lastPlayed = videoPlayerHistoryData.lastPlayed.toLong()\n    }\n\n    LaunchedEffect(videoPlayerVideoInfoData.width, videoPlayerVideoInfoData.height) {\n        val newAspectRatio =\n            videoPlayerVideoInfoData.width / videoPlayerVideoInfoData.height.toFloat()\n        defaultAspectRatio = newAspectRatio.takeIf { it > 0 } ?: (16 / 9f)\n        updateVideoAspectRatio()\n    }\n\n    val updateBackToHistory: () -> Unit = {\n        // 此处使用 videoPlayerHistoryData.lastPlayed 无法获取到新值\n        //if (videoPlayerHistoryData.lastPlayed > 0 && hideBackToHistoryTimer == null) {\n        if (lastPlayed > 0 && hideBackToHistoryTimer == null) {\n            logger.info { \"show showBackToHistory: ${videoPlayerHistoryData.lastPlayed}\" }\n            scope.launch(Dispatchers.Main) {\n                showBackToHistory = true\n                hideBackToHistoryTimer = countDownTimer(5000, 1000, \"hideBackToHistoryTimer\") {\n                    scope.launch(Dispatchers.Main) {\n                        showBackToHistory = false\n                        hideBackToHistoryTimer = null\n                        //playerViewModel.lastPlayed = 0\n                        onClearBackToHistoryData()\n                    }\n                }\n            }\n        }\n    }\n\n    val videoPlayerListener = object : VideoPlayerListener {\n        override fun onError(error: Exception) {\n            logger.info { \"onError: $error\" }\n            if (videoPlayerConfigData.isLive) {\n                // 直播模式：自动重连，不立即显示错误 UI（参考 wiliwili 的 retryRequestData）\n                logger.info { \"Live mode: triggering auto retry\" }\n                scope.launch(Dispatchers.Main) {\n                    isBuffering = true  // 显示缓冲状态代替错误状态\n                }\n                onLiveRetry()\n            } else {\n                scope.launch(Dispatchers.Main) {\n                    isError = true\n                    exception = error.cause as Exception?\n                }\n            }\n        }\n\n        override fun onReady() {\n            logger.info { \"onReady\" }\n            scope.launch(Dispatchers.Main) {\n                isError = false\n                exception = null\n                syncDanmakuConfig()\n                updateVideoAspectRatio()\n\n                //reset default play speed\n                onPlaySpeedChange(currentPlaySpeed)\n                logger.info { \"Reset default play speed: $currentPlaySpeed\" }\n                videoPlayer.speed = currentPlaySpeed\n            }\n        }\n\n        override fun onPlay() {\n            logger.info { \"onPlay\" }\n            scope.launch(Dispatchers.Main) {\n                isPlaying = true\n                isBuffering = false\n                danmakuView.play()\n                updateBackToHistory()\n            }\n        }\n\n        override fun onPause() {\n            logger.info { \"onPause\" }\n            scope.launch(Dispatchers.Main) {\n                isPlaying = false\n            }\n        }\n\n        override fun onBuffering() {\n            logger.info { \"onBuffering\" }\n            scope.launch(Dispatchers.Main) {\n                isBuffering = true\n            }\n        }\n\n        override fun onEnd() {\n            if (videoPlayerConfigData.showRelatedVideos) {\n                logger.info { \"onEnd: show related videos, skip auto next\" }\n                scope.launch(Dispatchers.Main) {\n                    isPlaying = false\n                }\n                return\n            }\n\n            if (videoPlayerConfigData.currentPlayMode == PlayMode.SingleLoop) {\n                logger.info { \"onEnd: replay\" }\n                scope.launch(Dispatchers.Main) {\n                    videoPlayer.seekTo(0)\n                    danmakuView.notifySeek(0)\n                    videoPlayer.start()\n                }\n                return\n            }\n\n            logger.info { \"onEnd\" }\n            scope.launch(Dispatchers.Main) {\n                isPlaying = false\n                if (!videoPlayerConfigData.incognitoMode && !videoPlayerConfigData.isLive) sendHeartbeat(scope, true)\n                if (!showInfoProvider()) {\n                    onLoadNextVideo(false)\n                } else {\n                    logger.info { \"Skip auto next because info panel visible\" }\n                }\n            }\n        }\n\n        override fun onIdle() {\n            //TODO(\"Not yet implemented\")\n        }\n\n        override fun onSeekBack(seekBackIncrementMs: Long) {\n            danmakuView.notifySeek(seekState.position)\n        }\n\n        override fun onSeekForward(seekForwardIncrementMs: Long) {\n            danmakuView.notifySeek(seekState.position)\n        }\n\n        override fun onVideoSizeChanged(width: Int, height: Int) {\n            logger.info { \"onVideoSizeChanged: ${width}x${height}\" }\n            if (width > 0 && height > 0) {\n                scope.launch(Dispatchers.Main) {\n                    val newDefaultAspectRatio =width / height.toFloat()\n                    if (newDefaultAspectRatio != defaultAspectRatio ) {\n                        defaultAspectRatio = newDefaultAspectRatio\n                        updateVideoAspectRatio()\n                    }\n                }\n            }\n        }\n    }\n\n    // 进度轮询：播放时每 200ms 更新进度、检查跳过片头片尾\n    LaunchedEffect(isPlaying, videoPlayerConfigData.isLive) {\n        while (isPlaying && !videoPlayerConfigData.isLive) {\n            val pos = videoPlayer.currentPosition.coerceAtLeast(0L)\n            val dur = videoPlayer.duration.coerceAtLeast(0L)\n            val buf = videoPlayer.bufferedPercentage.coerceIn(0, 100)\n\n            if (seekState.position != pos) seekState.position = pos\n            if (seekState.duration != dur) seekState.duration = dur\n            if (seekState.bufferedPercentage != buf) seekState.bufferedPercentage = buf\n\n            checkSkipTask(pos)\n\n            delay(200)\n        }\n    }\n\n    // 弹幕蒙版跟踪：独立轮询，蒙版活跃时自适应高频率，不影响进度更新和跳过检测\n    LaunchedEffect(isPlaying, videoPlayerConfigData.currentDanmakuMask, videoPlayerDanmakuMaskData.danmakuMasks.size) {\n        if (!videoPlayerConfigData.currentDanmakuMask || videoPlayerDanmakuMaskData.danmakuMasks.isEmpty()) {\n            if (currentDanmakuMaskFrame != null) currentDanmakuMaskFrame = null\n            return@LaunchedEffect\n        }\n        while (isPlaying) {\n            val pos = videoPlayer.currentPosition.coerceAtLeast(0L)\n            val newMask = DanmakuMaskFinder.findMaskFrame(\n                videoPlayerDanmakuMaskData.danmakuMasks,\n                pos\n            )\n            if (currentDanmakuMaskFrame != newMask) {\n                // logger.fInfo { \"Danmaku mask changed: ${currentDanmakuMaskFrame?.range}, new: ${newMask?.range}, current pos ${pos}ms\" }\n                currentDanmakuMaskFrame = newMask\n            }\n            // 有蒙版帧时精确对齐帧过期时刻；无匹配帧时低频轮询\n            val nextDelay = if (newMask != null) {\n                // logger.fInfo { \"Danmaku mask active: ${newMask.range.start}, next change at ${newMask.range.last}ms, current pos ${pos}ms\" }\n                (newMask.range.last - pos + 3).coerceIn(33, 200)\n            } else {\n                200L\n            }\n            delay(nextDelay)\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus(scope)\n    }\n\n\n\n    LaunchedEffect(videoPlayerLoadStateData.loadState) {\n        when (videoPlayerLoadStateData.loadState) {\n            RequestState.Ready -> {}\n            RequestState.Doing -> {}\n            RequestState.Done -> {}\n            RequestState.Success -> {}\n            RequestState.Failed -> {\n                exception = Exception(videoPlayerLoadStateData.errorMessage)\n                isError = true\n            }\n        }\n    }\n\n    // 心跳定时器：使用 LaunchedEffect 替代 java.util.Timer，避免后台线程与主线程双重分发导致 ANR\n    if (!videoPlayerConfigData.incognitoMode && !videoPlayerConfigData.isLive) {\n        DisposableEffect(Unit) {\n            val job = scope.launch {\n                delay(5000)\n                while (isActive) {\n                    if (videoPlayer.isPlaying) sendHeartbeat(scope, false)\n                    delay(15000)\n                }\n            }\n            onDispose {\n                job.cancel()\n                println(\"心跳定时器 cancel\")\n            }\n        }\n    }\n\n    // LaunchedEffect(videoPlayerLogsData.logs) {\n    //     showLogs = videoPlayerLogsData.logs.isNotEmpty()\n    //     if (showLogs) {\n    //         delay(3000)\n    //         showLogs = false\n    //     }\n    // }\n\n    DisposableEffect(Unit) {\n        onDispose {\n            videoPlayer.release()\n        }\n    }\n\n    DisposableEffect(showInfoProvider()) {\n        clockRefreshTimer?.cancel()\n        if (showInfoProvider()) {\n            clockRefreshTimer = countDownTimer(\n                millisInFuture = Long.MAX_VALUE,\n                countDownInterval = 1000,\n                tag = \"clockRefreshTimer\",\n                showLogs = false,\n                onTick = {\n                    val calendar = Calendar.getInstance()\n                    val hour = calendar.get(Calendar.HOUR_OF_DAY)\n                    val minute = calendar.get(Calendar.MINUTE)\n                    val second = calendar.get(Calendar.SECOND)\n                    if (clockState.hour != hour) clockState.hour = hour\n                    if (clockState.minute != minute) clockState.minute = minute\n                    if (clockState.second != second) clockState.second = second\n                }\n            )\n        }\n        onDispose { clockRefreshTimer?.cancel() }\n    }\n\n    val animatedAspectRatio by animateFloatAsState(\n        targetValue = aspectRatioValue,\n        animationSpec = tween(),\n        label = \"animatedAspectRatio\"\n    )\n\n    val videoPlayerModifier = when (currentVideoAspectRatio) {\n        VideoAspectRatio.EqualWidth -> Modifier\n            .fillMaxWidth()\n            .aspectRatio(animatedAspectRatio)\n\n        VideoAspectRatio.EqualHeight -> Modifier\n            .fillMaxHeight()\n            .aspectRatio(animatedAspectRatio, matchHeightConstraintsFirst = true)\n\n        VideoAspectRatio.Stretch -> Modifier.fillMaxSize()\n\n        else -> Modifier.aspectRatio(animatedAspectRatio)\n    }\n\n    CompositionLocalProvider(\n        LocalVideoPlayerSeekState provides seekState,\n        LocalVideoPlayerClockState provides clockState,\n        //LocalVideoPlayerHistoryData provides LocalVideoPlayerHistoryData.current.copy(\n        //    showBackToHistory = showBackToHistory\n        //),\n        //LocalVideoPlayerHistoryData provides VideoPlayerHistoryData(\n        //    lastPlayed = videoPlayerHistoryData.lastPlayed,\n        //    showBackToHistory = showBackToHistory\n        //),\n        LocalVideoPlayerStateData provides VideoPlayerStateData(\n            isPlaying = isPlaying,\n            isBuffering = isBuffering,\n            isError = isError,\n            exception = exception,\n            showBackToHistory = showBackToHistory\n        ),\n        LocalVideoPlayerDebugInfoData provides VideoPlayerDebugInfoData(\n            debugInfo = videoPlayer.debugInfo\n        ),\n    ) {\n        VideoPlayerController(\n            modifier = modifier\n                .focusRequester(focusRequester)\n                .fillMaxSize(),\n            videoPlayer = videoPlayer,\n            playerSeekForwardStep = playerSeekForwardStep,\n            playerSeekBackwardStep = playerSeekBackwardStep,\n            showBottomProgressBar = showBottomProgressBar,\n            showRelatedVideos = videoPlayerConfigData.showRelatedVideos,\n            onToggleRelatedVideos = onToggleRelatedVideos,\n            registerShowInfoProvider = { provider -> showInfoProvider = provider },\n            onViewerCountTipCanShowChanged = onViewerCountTipCanShowChanged,\n            viewerCountText = viewerCountText,\n\n            onPlay = { videoPlayer.start() },\n            onPause = {\n                videoPlayer.pause()\n                if (!videoPlayerConfigData.incognitoMode && !videoPlayerConfigData.isLive) sendHeartbeat(scope, false)\n            },\n            onExit = {\n                videoPlayer.pause()\n                if (!videoPlayerConfigData.incognitoMode && !videoPlayerConfigData.isLive) sendHeartbeat(scope, true)\n                onExit()\n            },\n            onGoTime = {\n                videoPlayer.seekTo(it)\n                danmakuView.notifySeek(it)\n            },\n            onBackToHistory = {\n                val time = if (videoPlayerConfigData.defaultStartPosition == DefaultStartPosition.History) {\n                    0L\n                } else {\n                    videoPlayerHistoryData.lastPlayed.toLong()\n                }\n                logger.fInfo { \"Back to history/beginning: ${time.formatHourMinSec()}\" }\n                videoPlayer.seekTo(time)\n                danmakuView.notifySeek(time)\n                //playerViewModel.lastPlayed = 0\n                onClearBackToHistoryData()\n                showBackToHistory = false\n                hideBackToHistoryTimer?.cancel()\n                hideBackToHistoryTimer = null\n            },\n            onPlayNewVideo = {\n                if (!videoPlayerConfigData.incognitoMode && !videoPlayerConfigData.isLive) sendHeartbeat(scope, false)\n                //playerViewModel.partTitle = it.title\n                //playerViewModel.loadPlayUrl(\n                //    avid = it.aid,\n                //    cid = it.cid,\n                //    epid = it.epid,\n                //    seasonId = it.seasonId,\n                //    continuePlayNext = true\n                //)\n                onLoadNewVideo(it)\n            },\n            onResolutionChange = { resolution ->\n                videoPlayer.pause()\n                val current = videoPlayer.currentPosition\n                onResolutionChange(resolution) {\n                    //scope.launch(Dispatchers.Default) {\n                    //    playerViewModel.updateAvailableCodec()\n                    //    playerViewModel.playQuality(qualityId)\n                    withContext(Dispatchers.Main) {\n                        videoPlayer.seekTo(current)\n                        videoPlayer.start()\n                    }\n                    //}\n                }\n                //playerViewModel.currentQuality = qualityId\n            },\n            onCodecChange = { videoCodec ->\n                videoPlayer.pause()\n                val current = videoPlayer.currentPosition\n                onCodecChange(videoCodec) {\n                    withContext(Dispatchers.Main) {\n                        videoPlayer.seekTo(current)\n                        videoPlayer.start()\n                    }\n                }\n            },\n            onAspectRatioChange = { aspectRadio ->\n                currentVideoAspectRatio = aspectRadio\n                onAspectRatioChange(currentVideoAspectRatio)\n                updateVideoAspectRatio()\n            },\n            onRotationChange = { rotation ->\n//                if (videoPlayerConfigData.currentResolution > Resolution.R1080P60) {\n//                    // 4k及以上的视频旋转后画面很卡、hdr、杜比世界的视频旋转后色彩和对比度不对， 所以先切换到<=R1080P60\n//                    val tempList =\n//                        videoPlayerConfigData.availableResolutions.sortedByDescending { it.code }\n//                    val currentQuality = tempList.firstOrNull { it.code <= Resolution.R1080P60.code }\n//                        ?: tempList.last()\n//                    if (videoPlayerConfigData.currentResolution != currentQuality) {\n//                        videoPlayer.pause()\n//                        val current = videoPlayer.currentPosition\n//                        onResolutionChange(currentQuality) {\n//                            withContext(Dispatchers.Main) {\n//                                videoPlayer.seekTo(current)\n//                                videoPlayer.start()\n//                            }\n//                        }\n//                    }\n//                }\n\n                currentVideoRotation = rotation\n                onRotationChange(rotation)\n            },\n            onPlaySpeedChange = { speed ->\n                logger.info { \"Set default play speed: $speed\" }\n                currentPlaySpeed = speed\n                onPlaySpeedChange(speed)\n                videoPlayer.speed = speed\n            },\n            onAudioChange = { audio ->\n                videoPlayer.pause()\n                val current = videoPlayer.currentPosition\n                onAudioChange(audio) {\n                    withContext(Dispatchers.Main) {\n                        videoPlayer.seekTo(current)\n                        videoPlayer.start()\n                    }\n                }\n            },\n            onLiveQualityChange = onLiveQualityChange,\n            onLiveCodecChange = onLiveCodecChange,\n            onDanmakuSwitchChange = { enabledDanmakuTypes ->\n                logger.info { \"On enabled danmaku type change: $enabledDanmakuTypes\" }\n                onDanmakuSwitchChange(enabledDanmakuTypes)\n                updateDanmakuConfigTypeFilter()\n            },\n            onDanmakuSizeChange = { scale ->\n                logger.info { \"On danmaku scale change: $scale\" }\n                onDanmakuSizeChange(scale)\n                applyDanmakuConfig(danmakuConfig.copy(textSizeScale = (scale * 100).toInt()))\n            },\n            onDanmakuOpacityChange = { opacity ->\n                logger.info { \"On danmaku opacity change: $opacity\" }\n                onDanmakuOpacityChange(opacity)\n                applyDanmakuConfig(danmakuConfig.copy(opacity = opacity))\n            },\n            onDanmakuAreaChange = { area ->\n                logger.info { \"On danmaku area change: $area\" }\n                onDanmakuAreaChange(area)\n                applyDanmakuConfig(danmakuConfig.copy(area = area))\n            },\n            onDanmakuMaskChange = { mask ->\n                logger.info { \"On danmaku mask change: $mask\" }\n                onDanmakuMaskChange(mask)\n            },\n            onDanmakuFilterLevelChange = { filterLevel ->\n                logger.info { \"On danmaku filter level change: $filterLevel\" }\n                applyDanmakuConfig(danmakuConfig.copy(minLevel = filterLevel))\n                onDanmakuFilterLevelChange(filterLevel)\n            },\n            onDanmakuRollingDurationFactorChange = { factor ->\n                logger.info { \"On danmaku rolling duration factor change: $factor\" }\n                onDanmakuRollingDurationFactorChange(factor)\n                val durationMultiplier = 2f - factor\n                applyDanmakuConfig(danmakuConfig.copy(durationMultiplier = durationMultiplier))\n            },\n            onSubtitleChange = { subtitle ->\n                onSubtitleChange(subtitle)\n            },\n            onSubtitleSizeChange = { size ->\n                logger.info { \"On subtitle font size change: $size\" }\n                onSubtitleSizeChange(size)\n            },\n            onSubtitleBackgroundOpacityChange = { opacity ->\n                logger.info { \"On subtitle background opacity change: $opacity\" }\n                onSubtitleBackgroundOpacityChange(opacity)\n            },\n            onSubtitleBottomPadding = { padding ->\n                logger.info { \"On subtitle bottom padding change: $padding\" }\n                onSubtitleBottomPadding(padding)\n            },\n            onPlayModeChange = { playMode ->\n                logger.info { \"On play mode change: $playMode\" }\n                onPlayModeChange(playMode)\n            },\n            onDebugInfoChange = { enabled ->\n                logger.info { \"On debug info change: $enabled\" }\n                onDebugInfoChange(enabled)\n            },\n            onRequestFocus = { focusRequester.requestFocus(scope) },\n            onOpenUpSpace = onOpenUpSpace,\n            onRefreshVideo = onRefreshVideo,\n            onOpenDanmaku = {\n                onShowDanmakuChange(true)\n                videoPlayerConfigData.showDanmaku = true\n                applyDanmakuConfig(danmakuConfig.copy(enabled = true))\n            },\n            onHideDanmaku = {\n                onShowDanmakuChange(false)\n                videoPlayerConfigData.showDanmaku = false\n                applyDanmakuConfig(danmakuConfig.copy(enabled = false))\n            },\n            userActionContent = userActionContent,\n            onLoadNextVideo = onLoadNextVideo,\n            onShowComment = onShowComment,\n            onShowDescription = onShowDescription\n        ) {\n            LaunchedEffect(Unit) {\n                videoPlayer.setOptions()\n            }\n\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .background(Color.Black)\n            )\n\n            BvVideoPlayer(\n                modifier = videoPlayerModifier.align(Alignment.Center),\n                videoPlayer = videoPlayer,\n                playerListener = videoPlayerListener,\n                rotationDegrees = currentVideoRotation.degrees,\n                forceUseTextureView = useTextureViewFixPortraitVideo\n            )\n\n            // 新弹幕引擎：直接使用 AndroidView wrapping DanmakuView\n            androidx.compose.ui.viewinterop.AndroidView(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .align(Alignment.TopCenter),\n                factory = { _ ->\n                    danmakuView.apply {\n                        setPositionProvider { if(currentConfigData.isLive) SystemClock.elapsedRealtime() else videoPlayer.currentPosition.coerceAtLeast(0L) }\n                        setIsPlayingProvider { videoPlayer.isPlaying }\n                        setPlaybackSpeedProvider { currentPlaySpeed }\n                        setConfig(danmakuConfig)\n                    }\n                },\n                update = { view ->\n                    view.setMaskFrame(currentDanmakuMaskFrame.takeIf { videoPlayerConfigData.currentDanmakuMask })\n                    view.setVideoAspectRatio(aspectRatioValue)\n                    view.setVideoAspectRatioType(currentVideoAspectRatio)\n                }\n            )\n\n            // 跳过片头片尾提示\n            if (showSkipOpTip) {\n                SkipOpTip(\n                    modifier = Modifier.align(Alignment.BottomStart),\n                    show = true,\n                    text = skipOpTipText\n                )\n            }\n            if (showSkipEdTip) {\n                SkipEdTip(\n                    modifier = Modifier.align(Alignment.BottomStart),\n                    show = true,\n                    text = skipEdTipText\n                )\n            }\n\n            if (showLogs) {\n                Column(\n                    modifier = Modifier.align(Alignment.BottomStart)\n                ) {\n                    Text(text = videoPlayerLogsData.logs)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/SeekBar.kt",
    "content": "package dev.aaa1115910.bv.player.tv\n\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.SliderColors\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.tooling.preview.PreviewParameter\nimport androidx.compose.ui.tooling.preview.PreviewParameterProvider\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.constraintlayout.compose.ConstraintLayout\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.Text\nimport androidx.tv.material3.darkColorScheme\nimport dev.aaa1115910.bv.player.seekbar.SeekBar\nimport dev.aaa1115910.bv.player.seekbar.SeekBarThumb\nimport dev.aaa1115910.bv.player.seekbar.SeekMoveState\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport kotlin.math.max\n\n@Composable\nfun VideoSeekBar(\n    modifier: Modifier = Modifier,\n    duration: Long,\n    position: Long,\n    bufferedPercentage: Int,\n    idleIcon: String = \"\",\n    movingIcon: String = \"\",\n    moveState: SeekMoveState = SeekMoveState.Idle,\n    showPosition: Boolean = false,\n    isFocused: Boolean = false,\n    strokeWidth: Dp = 0.dp\n) {\n    VideoSeekBar(\n        modifier = modifier,\n        duration = duration,\n        position = position,\n        bufferedPercentage = bufferedPercentage,\n        useDefaultThumb = idleIcon.isBlank(),\n        showPosition = showPosition,\n        thumb = { thumbModifier ->\n            SeekBarThumb(\n                modifier = thumbModifier,\n                state = moveState,\n                idleJsonUrl = idleIcon,\n                movingJsonUrl = movingIcon\n            )\n        },\n        isFocused = isFocused,\n        strokeWidth = strokeWidth\n    )\n}\n\n@Composable\nprivate fun VideoSeekBar(\n    modifier: Modifier = Modifier,\n    duration: Long,\n    position: Long,\n    bufferedPercentage: Int,\n    colors: SliderColors = SliderDefaults.colors(),\n    useDefaultThumb: Boolean = false,\n    showPosition: Boolean = false,\n    thumb: (@Composable (Modifier) -> Unit)? = null,\n    isFocused: Boolean = false,\n    strokeWidth: Dp = 0.dp\n) {\n    BoxWithConstraints(\n        modifier = modifier\n    ) {\n        val width = this.maxWidth\n\n        ConstraintLayout(\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            val (positionText, seek, thumbIcon) = createRefs()\n\n            SeekBar(\n                modifier = Modifier\n                    .constrainAs(seek) {\n                        start.linkTo(parent.start)\n                        end.linkTo(parent.end)\n                        bottom.linkTo(parent.bottom, 8.dp)\n                    }\n                    .border(\n                        width = 1.dp,\n                        color = if (isFocused) Color.White.copy(alpha = 0.3f) else Color.Transparent,\n                        shape = RoundedCornerShape(6.dp)\n                    )\n                    .padding(horizontal = 6.dp, vertical = 1.dp),\n                duration = duration,\n                position = position,\n                bufferedPercentage = bufferedPercentage,\n                colors = colors,\n                height = if (strokeWidth > 0.dp) strokeWidth else 10.dp,\n                strokeWidth = if (strokeWidth > 0.dp) strokeWidth else if (isFocused) 10.dp else 4.dp\n            )\n            thumb?.invoke(\n                Modifier\n                    .constrainAs(thumbIcon) {\n                        start.linkTo(\n                            parent.start,\n                            (width - 48.dp) * (position / max(duration.toFloat(), 1f))\n                        )\n                        bottom.linkTo(seek.bottom)\n                        top.linkTo(seek.top)\n                    }\n            )\n            if (showPosition) {\n                Text(\n                    text = position.formatHourMinSec(),\n                    modifier = Modifier.constrainAs(positionText) {\n                        start.linkTo(thumbIcon.start)\n                        end.linkTo(thumbIcon.end)\n                        bottom.linkTo(thumbIcon.top)\n                    }\n                )\n            }\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun SeekWithThumbPreview(@PreviewParameter(ProgressProvider::class) data: Triple<Long, Long, Int>) {\n    MaterialTheme(\n        colorScheme = darkColorScheme()\n    ) {\n        Surface {\n            VideoSeekBar(\n                duration = data.first,\n                position = data.second,\n                bufferedPercentage = data.third,\n                showPosition = true,\n                thumb = { modifier ->\n                    SeekBarThumb(\n                        modifier = modifier,\n                        state = SeekMoveState.Idle,\n                        idleJsonUrl = \"https://i0.hdslb.com/bfs/garb/item/df917f079cd8175cc851cd1e19a197d810a1c6b7.json\",\n                        movingJsonUrl = \"https://i0.hdslb.com/bfs/garb/item/b61bb387a4c895ef165798102ef322c631a9e4e1.json\"\n                    )\n                },\n                isFocused = true\n            )\n        }\n\n    }\n}\n\nprivate class ProgressProvider : PreviewParameterProvider<Triple<Long, Long, Int>> {\n    override val values = sequenceOf(\n        Triple(1234_000L, 0L, 3),\n        Triple(1234_000L, 234_000L, 24),\n        Triple(1234_000L, 555_000L, 57),\n        Triple(1234_000L, 1234_000L, 100)\n    )\n}\n"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/BottomSubtitle.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekState\n\n@Composable\nfun BottomSubtitle(\n    modifier: Modifier = Modifier\n) {\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val videoPlayerSeekState = LocalVideoPlayerSeekState.current\n    val subtitleData = videoPlayerConfigData.currentSubtitleData\n    val time = videoPlayerSeekState.position\n\n    var currentText by remember { mutableStateOf(\"\") }\n    var isAI by remember { mutableStateOf(false) }\n\n    val updateCurrentText: () -> Unit = {\n        runCatching {\n            val currentItem = subtitleData.find { it.isShowing(time) }\n            currentText = currentItem?.content ?: \"\"\n            isAI = currentItem?.isAI ?: false\n        }\n    }\n\n    LaunchedEffect(time) {\n        updateCurrentText()\n    }\n\n    Box(\n        modifier = modifier.fillMaxSize()\n    ) {\n        if (currentText != \"\") {\n            Row(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .padding(bottom = videoPlayerConfigData.currentSubtitleBottomPadding)\n            ) {\n                Text(\n                    modifier = Modifier\n                        .clip(MaterialTheme.shapes.small)\n                        .background(Color.Black.copy(alpha = videoPlayerConfigData.currentSubtitleBackgroundOpacity))\n                        .padding(vertical = 4.dp, horizontal = 12.dp),\n                    text = currentText,\n                    fontSize = videoPlayerConfigData.currentSubtitleFontSize,\n                    textAlign = TextAlign.Center\n                )\n                if (isAI) {\n                    Text(\n                        modifier = Modifier\n                            .padding(start = 4.dp, top = 4.dp)\n                            .alpha(0.5f),\n                        text = \"AI\",\n                        fontSize = (videoPlayerConfigData.currentSubtitleFontSize.value / 3).sp,\n                        color = Color.White\n                    )\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/ControllerVideoInfo.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport android.os.CountDownTimer\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.automirrored.rounded.PlaylistPlay\nimport androidx.compose.material.icons.outlined.Settings\nimport androidx.compose.material.icons.rounded.KeyboardDoubleArrowDown\nimport androidx.compose.material.icons.rounded.Refresh\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.graphics.Shadow\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Comment\nimport androidx.compose.material.icons.outlined.Info\nimport androidx.compose.material.icons.rounded.ArrowDropDown\nimport androidx.compose.material.icons.rounded.ArrowDropUp\nimport androidx.compose.material.icons.twotone.ScreenRotation\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.ui.focus.focusProperties\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.window.Dialog\nimport androidx.tv.material3.Border\nimport androidx.tv.material3.Button\nimport androidx.tv.material3.ButtonDefaults\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.video.Subtitle\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerClockState\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.parseControllerButtonsOrder\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekState\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekThumbData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerStateData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerVideoInfoData\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoPlayerClockState\nimport dev.aaa1115910.bv.player.entity.VideoPlayerSeekState\nimport dev.aaa1115910.bv.player.entity.VideoPlayerSeekThumbData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerStateData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerVideoInfoData\nimport dev.aaa1115910.bv.player.entity.VideoRotation\nimport dev.aaa1115910.bv.player.seekbar.SeekMoveState\nimport dev.aaa1115910.bv.player.shared.R\nimport dev.aaa1115910.bv.player.tv.VideoSeekBar\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport dev.aaa1115910.bv.util.ifElse\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport dev.aaa1115910.bv.util.requestFocus\nimport kotlinx.coroutines.Job\nimport kotlin.math.roundToInt\n\nprivate fun formatSpeed(speed: Float): String {\n    return \"${(speed * 100).roundToInt() / 100f}x\"\n}\n\n@Composable\nfun ControllerVideoInfo(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    playSpeed: Float = 1f,\n    onHideInfo: () -> Unit,\n    onPlay: () -> Unit,\n    onPause: () -> Unit,\n    onPlaySpeedChange: (Float) -> Unit,\n    onOpenUpSpace: () -> Unit,\n    onRefreshVideo: () -> Unit,\n    onOpenDanmaku: () -> Unit,\n    onHideDanmaku: () -> Unit,\n    onOpenPlayList: () -> Unit,\n    onOpenRelatedVideo: () -> Unit,\n    onOpenSetting: () -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit,\n    onRotationChange: (VideoRotation) -> Unit,\n    userActionContent: UserActionContent = EmptyUserActionContent,\n    onSeekBack: () -> Unit,\n    onSeekForward: () -> Unit,\n    onSubtitleChange: (Subtitle) -> Unit,\n    onLoadNextVideo: (Boolean) -> Unit,\n    onShowComment: () -> Unit = {},\n    onShowDescription: () -> Unit = {},\n    onResolutionChange: (Resolution) -> Unit = {},\n    onLiveQualityChange: (Int) -> Unit = {},\n    viewerCountText: String = \"\",\n) {\n    val context = LocalContext.current\n    val videoPlayerClockState = LocalVideoPlayerClockState.current\n    val videoPlayerSeekState = LocalVideoPlayerSeekState.current\n    val videoPlayerSeekThumbData = LocalVideoPlayerSeekThumbData.current\n    val videoPlayerVideoInfoData = LocalVideoPlayerVideoInfoData.current\n    val videoPlayerStateData = LocalVideoPlayerStateData.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n\n    Box(\n        modifier = modifier.fillMaxSize()\n    ) {\n        AnimatedVisibility(\n            modifier = Modifier.align(Alignment.TopEnd),\n            visible = show,\n            enter = expandVertically(),\n            exit = shrinkVertically(),\n            label = \"ControllerTopVideoInfo\"\n        ) {\n            ControllerVideoInfoTop(\n                clock = Triple(\n                    videoPlayerClockState.hour,\n                    videoPlayerClockState.minute,\n                    videoPlayerClockState.second\n                )\n            )\n        }\n        AnimatedVisibility(\n            modifier = Modifier.align(Alignment.BottomCenter),\n            visible = show,\n            enter = expandVertically(),\n            exit = shrinkVertically(),\n            label = \"ControllerBottomVideoInfo\"\n        ) {\n            ControllerVideoInfoBottom(\n                show = show,\n                onHideInfo = onHideInfo,\n                seekData = videoPlayerSeekState,\n                stateData = videoPlayerStateData,\n                title = videoPlayerVideoInfoData.title,\n                partTitle = videoPlayerVideoInfoData.partTitle,\n                playSpeed = playSpeed,\n                rotation = videoPlayerConfigData.currentVideoRotation,\n                idleIcon = videoPlayerSeekThumbData.idleIcon,\n                movingIcon = videoPlayerSeekThumbData.movingIcon,\n                play = videoPlayerVideoInfoData.play,\n                danmaku = videoPlayerVideoInfoData.danmaku,\n                like = videoPlayerVideoInfoData.like,\n                coin = videoPlayerVideoInfoData.coin,\n                favorite = videoPlayerVideoInfoData.favorite,\n                upName = videoPlayerVideoInfoData.upName,\n                pubTime = videoPlayerVideoInfoData.pubTime,\n                isPlaying = videoPlayerStateData.isPlaying || videoPlayerStateData.isBuffering,\n                currentPlayMode = videoPlayerConfigData.currentPlayMode,\n                hasPreloadedVideoList = videoPlayerConfigData.hasPreloadedVideoList,\n                hasRelatedVideos = videoPlayerConfigData.hasRelatedVideos,\n                showDanmaku = videoPlayerConfigData.showDanmaku,\n                onPlay = onPlay,\n                onPause = onPause,\n                onPlaySpeedChange = onPlaySpeedChange,\n                onOpenUpSpace = onOpenUpSpace,\n                onRefreshVideo = onRefreshVideo,\n                onOpenDanmaku = onOpenDanmaku,\n                onHideDanmaku = onHideDanmaku,\n                onOpenPlayList = onOpenPlayList,\n                onOpenRelatedVideo = onOpenRelatedVideo,\n                onOpenSetting = onOpenSetting,\n                onPlayModeChange = onPlayModeChange,\n                onRotationChange = onRotationChange,\n                fromSeason = videoPlayerVideoInfoData.fromSeason,\n                isLive = videoPlayerConfigData.isLive,\n                userActionContent = userActionContent,\n                onSeekBack = onSeekBack,\n                onSeekForward = onSeekForward,\n                availableSubtitleTracks = videoPlayerConfigData.availableSubtitleTracks,\n                currentSubtitleId = videoPlayerConfigData.currentSubtitleId,\n                onSubtitleChange = { id ->\n                    val track = videoPlayerConfigData.availableSubtitleTracks.firstOrNull { it.id == id }\n                    track?.let { onSubtitleChange(it) }\n                },\n                isFollowingUp = videoPlayerVideoInfoData.isFollowingUp,\n                showNextVideoBtn = videoPlayerConfigData.showNextVideoBtn,\n                onLoadNextVideo = onLoadNextVideo,\n                onShowComment = onShowComment,\n                onShowDescription = onShowDescription,\n                availableResolutions = videoPlayerConfigData.availableResolutions,\n                currentResolution = videoPlayerConfigData.currentResolution,\n                onResolutionChange = onResolutionChange,\n                availableLiveQualities = videoPlayerConfigData.availableLiveQualities,\n                currentLiveQn = videoPlayerConfigData.currentLiveQn,\n                currentLiveQualityDescription = videoPlayerConfigData.currentLiveQualityDescription,\n                onLiveQualityChange = onLiveQualityChange,\n                controllerButtonsOrder = videoPlayerConfigData.controllerButtonsOrder,\n                viewerCountText = viewerCountText\n            )\n        }\n    }\n}\n\n@Composable\nfun ControllerVideoInfoTop(\n    modifier: Modifier = Modifier,\n    clock: Triple<Int, Int, Int>\n) {\n    Clock(\n        modifier = modifier\n            .padding(horizontal = 32.dp, vertical = 16.dp),\n        hour = clock.first,\n        minute = clock.second,\n        second = clock.third\n    )\n}\n\ndata class ControlButton(\n    val id: String,\n    val icon: ImageVector? = null,\n    val text: String? = null,\n    val onClick: () -> Unit,\n    val visible: Boolean = true,\n    val scale: Float = 1f,\n    val painterId: Int? = null,\n    val tint: Color = Color.White.copy(alpha = 0.8f),\n    val width: Int? = null,\n    val fontWeight: FontWeight? = null\n)\n\n@Composable\nfun ControllerVideoInfoBottom(\n    show: Boolean,\n    onHideInfo: () -> Unit,\n    modifier: Modifier = Modifier,\n    playSpeed: Float = 1f,\n    rotation: VideoRotation,\n    title: String,\n    partTitle: String,\n    seekData: VideoPlayerSeekState,\n    stateData: VideoPlayerStateData,\n    idleIcon: String,\n    movingIcon: String,\n    play: Long,\n    danmaku: Int,\n    like: Int,\n    coin: Int,\n    favorite: Int,\n    upName: String,\n    pubTime: String,\n    isPlaying: Boolean,\n    currentPlayMode: PlayMode,\n    hasPreloadedVideoList: Boolean = false,\n    hasRelatedVideos: Boolean = false,\n    showDanmaku: Boolean,\n    onPlay: () -> Unit,\n    onPause: () -> Unit,\n    onPlaySpeedChange: (Float) -> Unit,\n    onOpenUpSpace: () -> Unit,\n    onRefreshVideo: () -> Unit,\n    onOpenDanmaku: () -> Unit,\n    onHideDanmaku: () -> Unit,\n    onOpenPlayList: () -> Unit,\n    onOpenRelatedVideo: () -> Unit,\n    onOpenSetting: () -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit,\n    onRotationChange: (VideoRotation) -> Unit,\n    fromSeason: Boolean = false,\n    isLive: Boolean = false,\n    isFollowingUp: Boolean = false,\n    userActionContent: UserActionContent = EmptyUserActionContent,\n    onSeekBack: () -> Unit,\n    onSeekForward: () -> Unit,\n    availableSubtitleTracks: List<Subtitle> = emptyList(),\n    currentSubtitleId: Long,\n    onSubtitleChange: (Long) -> Unit,\n    showNextVideoBtn: Boolean = false,\n    onLoadNextVideo: (Boolean) -> Unit,\n    onShowComment: () -> Unit = {},\n    onShowDescription: () -> Unit = {},\n    availableResolutions: List<Resolution> = emptyList(),\n    currentResolution: Resolution = Resolution.R240P,\n    onResolutionChange: (Resolution) -> Unit = {},\n    availableLiveQualities: List<Pair<Int, String>> = emptyList(),\n    currentLiveQn: Int = 0,\n    currentLiveQualityDescription: String = \"\",\n    onLiveQualityChange: (Int) -> Unit = {},\n    controllerButtonsOrder: String = \"\",\n    viewerCountText: String = \"\"\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    var hideVideoInfoJob by remember { mutableStateOf<Job?>(null) }\n    var pauseAutoHide by remember { mutableStateOf(false) }\n    var showSpeedDialog by remember { mutableStateOf(false) }\n    var showRotationDialog by remember { mutableStateOf(false) }\n    var showSubtitleDialog by remember { mutableStateOf(false) }\n    var showQualityDialog by remember { mutableStateOf(false) }\n    var showPlayModeDialog by remember { mutableStateOf(false) }\n    var speed by remember { mutableFloatStateOf(playSpeed) }\n    val danmakuIconId = if (showDanmaku) R.drawable.ic_danmaku_on else R.drawable.ic_danmaku_hide\n    val subtitleIconId = if (currentSubtitleId > -1) R.drawable.ic_subtitle_on else R.drawable.ic_subtitle_off\n    val upSpaceIconId = if (isFollowingUp) R.drawable.person_following else R.drawable.person\n    val buttonConfigs = remember(controllerButtonsOrder) {\n        parseControllerButtonsOrder(controllerButtonsOrder)\n    }\n\n    val currentQualityText = if (isLive) currentLiveQualityDescription.ifEmpty { \"画质\" } else currentResolution.getShortDisplayName(context).ifEmpty { \"画质\" }\n\n    val playModeIconId = when (currentPlayMode) {\n        PlayMode.SingleVideo -> R.drawable.ic_play_mode_single\n        PlayMode.SingleLoop -> R.drawable.ic_play_mode_single_loop\n        PlayMode.ListOrder -> R.drawable.ic_play_mode_list_order\n        PlayMode.ListOrderReverse -> R.drawable.ic_play_mode_list_order_reverse\n        PlayMode.PartAndEpisode -> R.drawable.ic_play_mode_part_and_episode\n        PlayMode.PartAndEpisodeReverse -> R.drawable.ic_play_mode_part_and_episode_reverse\n        PlayMode.RelatedVideo -> R.drawable.ic_play_mode_related_video\n        PlayMode.Custom -> R.drawable.ic_play_mode_custom\n    }\n\n    val buttons = remember(isLive, fromSeason, showDanmaku, currentPlayMode, speed, rotation, currentSubtitleId, isFollowingUp, showNextVideoBtn, currentLiveQualityDescription, currentResolution, availableResolutions, buttonConfigs) {\n        val rawButtons = listOf(\n            ControlButton(\n                id = \"nextVideo\",\n                painterId = R.drawable.next_play_fill,\n                scale = 0.7f,\n                onClick = { onLoadNextVideo(true) },\n                visible = showNextVideoBtn && !isLive\n            ),\n            ControlButton(\n                id = \"refresh\",\n                icon = Icons.Rounded.Refresh,\n                onClick = onRefreshVideo\n            ),\n            ControlButton(\n                id = \"speed\",\n                text = formatSpeed(speed),\n                onClick = { showSpeedDialog = true },\n                width = 46,\n                visible = !isLive\n            ),\n            ControlButton(\n                id = \"resolution\",\n                text = currentQualityText,\n                onClick = { showQualityDialog = true },\n                width = 46,\n                visible = (isLive && availableLiveQualities.isNotEmpty()) || (!isLive && availableResolutions.isNotEmpty())\n            ),\n            ControlButton(\n                id = \"upSpace\",\n                painterId = upSpaceIconId,\n                scale = 0.72f,\n                onClick = onOpenUpSpace,\n                visible = !fromSeason\n            ),\n            ControlButton(\n                id = \"rotation\",\n                icon = Icons.TwoTone.ScreenRotation,\n                onClick = { showRotationDialog = true },\n                scale = 0.75f\n            ),\n            ControlButton(\n                id = \"subtitle\",\n                painterId = subtitleIconId,\n                scale = 0.97f,\n                onClick = { showSubtitleDialog = true },\n                visible = availableSubtitleTracks.count() > 1 && !isLive\n            ),\n            ControlButton(\n                id = \"comment\",\n                icon = Icons.Outlined.Comment,\n                scale = 0.95f,\n                onClick = onShowComment,\n                fontWeight = FontWeight.Bold,\n                visible = !isLive\n            ),\n            ControlButton(\n                id = \"description\",\n                icon = Icons.Outlined.Info,\n                scale = 0.95f,\n                onClick = { onHideInfo(); onShowDescription() },\n                visible = !isLive\n            ),\n            ControlButton(\n                id = \"danmaku\",\n                painterId = danmakuIconId,\n                onClick = { if (showDanmaku) onHideDanmaku() else onOpenDanmaku() }\n            ),\n            ControlButton(\n                id = \"playMode\",\n                painterId = playModeIconId,\n                onClick = { showPlayModeDialog = true },\n                visible = !isLive\n            ),\n            ControlButton(\n                id = \"playlist\",\n                icon = Icons.AutoMirrored.Rounded.PlaylistPlay,\n                onClick = onOpenPlayList,\n                scale = 1.2f,\n                visible = !isLive\n            ),\n            ControlButton(\n                id = \"related\",\n                icon = Icons.Rounded.KeyboardDoubleArrowDown,\n                onClick = onOpenRelatedVideo,\n                visible = !fromSeason && !isLive\n            ),\n            ControlButton(\n                id = \"settings\",\n                icon = Icons.Outlined.Settings,\n                onClick = onOpenSetting,\n                scale = 0.9f\n            )\n        )\n\n        if (buttonConfigs.isEmpty()) {\n            rawButtons.filter { it.visible }\n        } else {\n            val buttonMap = rawButtons.associateBy { it.id }\n            val configIds = buttonConfigs.map { it.id }\n            val configMap = buttonConfigs.associateBy { it.id }\n            val ordered = configIds.mapNotNull { id ->\n                buttonMap[id]?.let { button ->\n                    if (configMap[id]?.hidden == true) button.copy(visible = false)\n                    else button\n                }\n            }\n            val remaining = rawButtons.filter { it.id !in configIds }\n            (ordered + remaining).filter { it.visible }\n        }\n    }\n\n    // 默认焦点按钮：用户设置的默认焦点，如果不可见则使用第一个可见按钮\n    val defaultFocusButtonId = remember(buttons, buttonConfigs) {\n        val configDefault = buttonConfigs.firstOrNull { it.isDefaultFocus }?.id\n        if (configDefault != null && buttons.any { it.id == configDefault }) {\n            configDefault\n        } else {\n            buttons.firstOrNull()?.id\n        }\n    }\n\n    val focusRequesters = remember(buttons) {\n        buttons.associate { button ->\n            button.id to FocusRequester()\n        }.toMutableMap()\n    }\n\n    // user action focus requesters (由 Controller 提供给调用方)。创建默认的四项：like/fav/coin/toview\n    val userActionFocusRequesters = remember {\n        mutableStateOf(\n            mapOf(\n                UserActionKey.Like to FocusRequester(),\n                UserActionKey.Favorite to FocusRequester(),\n                UserActionKey.Coin to FocusRequester(),\n                UserActionKey.ToView to FocusRequester()\n            )\n        )\n    }\n\n    val seekbarFocusRequester = remember { FocusRequester() }\n    var seekbarHasFocus by remember { mutableStateOf(false) }\n\n    fun formatStat(value: Long): String = if (value >= 10000) String.format(\"%.1f\", value / 10000.0) + \" 万\" else \"$value \"\n\n    val statString by remember(viewerCountText) {\n        mutableStateOf(\n            when {\n                upName.isNotEmpty() -> {\n                    val base = if (isLive) upName else {\n                        \"$upName  ·  ${formatStat(play)}播放  ·  ${formatStat(danmaku.toLong())}弹幕  ·  ${formatStat(like.toLong())}点赞  ·  ${formatStat(favorite.toLong())}收藏  ·  ${formatStat(coin.toLong())}投币  ·  发布于 $pubTime\"\n                    }\n                    if (viewerCountText.isNotEmpty()) \"$base  ·  $viewerCountText\" else base\n                }\n                viewerCountText.isNotEmpty() -> viewerCountText\n                else -> \"\"\n            }\n        )\n    }\n\n    LaunchedEffect(show, isLive) {\n        if (show) {\n            if (isLive) {\n                // 直播默认聚焦第一个按钮，因为直播进度条没用\n                defaultFocusButtonId\n                    ?.let { focusRequesters[it] }\n                    ?.requestFocus()\n            } else {\n                // 初始聚焦 进度条\n                seekbarFocusRequester.requestFocus()\n            }\n        }\n    }\n\n    fun cancelHideJob() {\n        hideVideoInfoJob?.cancel()\n        hideVideoInfoJob = null\n    }\n\n    fun scheduleHideJob() {\n        cancelHideJob()\n        if (show && !showSpeedDialog && !showRotationDialog && !showSubtitleDialog && !showQualityDialog && !showPlayModeDialog && !pauseAutoHide) {\n            hideVideoInfoJob = scope.launch {\n                delay(5000)\n                withContext(Dispatchers.Main) { onHideInfo() }\n            }\n        }\n    }\n\n    LaunchedEffect(show, showSpeedDialog, showRotationDialog, showSubtitleDialog, showQualityDialog, showPlayModeDialog, pauseAutoHide) {\n        scheduleHideJob()\n    }\n\n    Column(\n        modifier = modifier\n            .onPreviewKeyEvent { event ->\n                if (event.type == KeyEventType.KeyDown) {\n                    scheduleHideJob()\n                }\n                false\n            }\n            .background(\n                Brush.verticalGradient(\n                    colors = listOf(\n                        Color.Transparent,\n                        Color.Black.copy(alpha = 0.5f)\n                    ),\n                    endY = 136f\n                )\n            ),\n        verticalArrangement = Arrangement.Bottom\n    ) {\n        Spacer(\n            modifier = Modifier\n                .padding(top = 32.dp)\n        )\n        Text(\n            modifier = Modifier\n                .padding(horizontal = 32.dp),\n            text = \"${if(title.contains(partTitle)) \"\" else \"$partTitle ｜ \"}$title\",\n            color = Color.White,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis,\n            style = MaterialTheme.typography.headlineSmall,\n        )\n        if (statString.isNotEmpty()) {\n            Text(\n                modifier = Modifier\n                    .padding(start = 32.dp, end = 32.dp, top = 8.dp, bottom = 0.dp)\n                    .fillMaxWidth(),\n                text = statString,\n                color = Color.White,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                style = MaterialTheme.typography.bodyMedium\n            )\n        }\n        // 当前注入的是：点赞、收藏、投币、稍后再看\n        if (!isLive) {\n            userActionContent(\n                Modifier.focusProperties {\n                    down = seekbarFocusRequester\n                },\n                userActionFocusRequesters.value,\n                { id ->\n                    // 当用户 action 获得焦点时，设置当前聚焦 id 并重置自动隐藏计时\n                    scheduleHideJob()\n                },\n                { pause ->\n                    pauseAutoHide = pause\n                    if (pause) cancelHideJob() else scheduleHideJob()\n                }\n            )\n        }\n        VideoSeekBar(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(start = 30.dp, end = 30.dp, bottom = 2.dp)\n                .focusRequester(seekbarFocusRequester)\n                .onFocusChanged {\n                    scheduleHideJob()\n                    seekbarHasFocus = it.isFocused\n                }\n                .focusProperties {\n                    up = userActionFocusRequesters.value[UserActionKey.Like] ?: FocusRequester()\n                    down = defaultFocusButtonId?.let { focusRequesters[it] } ?: FocusRequester()\n                }\n                .ifElse(!isLive, Modifier.focusable())\n                .onPreviewKeyEvent {\n                    if (seekbarHasFocus && it.type == KeyEventType.KeyDown) {\n                        when (it.key) {\n                            Key.DirectionLeft -> onSeekBack()\n                            Key.DirectionRight -> onSeekForward()\n                            Key.Enter -> if (isPlaying) onPause() else onPlay()\n                            Key.DirectionCenter -> if (isPlaying) onPause() else onPlay()\n                        }\n                    }\n                    false\n                },\n            duration = seekData.duration,\n            position = seekData.position,\n            bufferedPercentage = seekData.bufferedPercentage,\n            moveState = SeekMoveState.Idle,\n            idleIcon = idleIcon,\n            movingIcon = movingIcon,\n            isFocused = seekbarHasFocus\n        )\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(start = 32.dp, end = 32.dp, top = 0.dp, bottom = 10.dp)\n                .ifElse(\n                    !isLive,\n                    Modifier.focusProperties {\n                        up = seekbarFocusRequester\n                    }\n                )\n                .onPreviewKeyEvent { event ->\n                    if (event.type == KeyEventType.KeyDown) {\n                        if (!fromSeason && !isLive && event.key == Key.DirectionDown) {\n                            onOpenRelatedVideo()\n                        }\n                    }\n                    false\n                },\n            verticalAlignment = Alignment.Top\n        ) {\n            buttons.forEachIndexed { index, button ->\n                Button(\n                    modifier = Modifier\n                        .height(30.dp)\n                        .width((button.width ?: 30).dp)\n                        .focusRequester(focusRequesters[button.id] ?: FocusRequester()),\n                    onClick = button.onClick,\n                    shape = ButtonDefaults.shape(shape = RoundedCornerShape(8.dp)),\n                    contentPadding = PaddingValues(vertical = 2.dp, horizontal = (if(button.text != null) 1 else 2).dp),\n                    colors = ButtonDefaults.colors(\n                        containerColor = Color.Transparent,\n                        focusedContainerColor = Color.White.copy(alpha = 0.3f)\n                    ),\n                    border = ButtonDefaults.border(\n                        border = Border(\n                            border = BorderStroke(\n                                width = 1.dp,\n                                color = Color.Transparent\n                            )\n                        ),\n                        focusedBorder = Border(\n                            border = BorderStroke(\n                                width = 1.dp,\n                                color = Color.White.copy(alpha = 0.45f)\n                            )\n                        )\n                    )\n                ) {\n                    if (button.text != null) {\n                        Text(\n                            text = button.text,\n                            textAlign = TextAlign.Center,\n                            style = MaterialTheme.typography.bodyLarge,\n                            color = button.tint,\n                            fontWeight = button.fontWeight,\n                            maxLines = 1,\n                            overflow = TextOverflow.Clip,\n                            modifier = Modifier\n                                .ifElse(\n                                    button.scale != 1f,\n                                    Modifier.scale(button.scale)\n                                )\n                        )\n                    } else if (button.painterId != null) {\n                        Icon(\n                            modifier = Modifier\n                                .ifElse(button.scale != 1f, Modifier.scale(button.scale)),\n                            painter = painterResource(id = button.painterId),\n                            contentDescription = null,\n                            tint = button.tint\n                        )\n                    } else {\n                        button.icon?.let {\n                            Icon(\n                                modifier = Modifier\n                                    .fillMaxSize()\n                                    .ifElse(button.scale != 1f, Modifier.scale(button.scale)),\n                                imageVector = it,\n                                contentDescription = null,\n                                tint = button.tint\n                            )\n                        }\n                    }\n                }\n                if (index < buttons.size - 1) {\n                    Spacer(Modifier.width(12.dp))\n                }\n            }\n\n            Spacer(Modifier.weight(1f))\n            Text(\n                modifier = Modifier\n                    .padding(top = 8.dp, bottom = 0.dp),\n                text = \"${seekData.position.formatHourMinSec()} / ${seekData.duration.formatHourMinSec()}\",\n                color = Color.White\n            )\n        }\n    }\n\n    if (showSpeedDialog) {\n        SpeedDialog(\n            onHideDialog = { showSpeedDialog = false },\n            speed = speed,\n            onSpeedChange = {\n                speed = it\n                onPlaySpeedChange(it)\n            }\n        )\n    }\n\n    if (showRotationDialog) {\n        RotationDialog(\n            onHideDialog = { showRotationDialog = false },\n            rotation = rotation,\n            onRotationChange = onRotationChange\n        )\n    }\n\n    if (showSubtitleDialog) {\n        val currentSubtitle = availableSubtitleTracks.firstOrNull { it.id == currentSubtitleId }\n        if (currentSubtitle != null) {\n            SubtitleDialog(\n                onHideDialog = { showSubtitleDialog = false },\n                subtitle = currentSubtitle,\n                availableSubtitleTracks = availableSubtitleTracks,\n                onSubtitleChange = { subtitle ->\n                    onSubtitleChange(subtitle.id)\n                }\n            )\n        }\n    }\n\n    if (showQualityDialog) {\n        if (isLive && availableLiveQualities.isNotEmpty()) {\n            LiveQualityDialog(\n                onHideDialog = { showQualityDialog = false },\n                availableLiveQualities = availableLiveQualities,\n                currentLiveQn = currentLiveQn,\n                onLiveQualityChange = onLiveQualityChange\n            )\n        } else if (!isLive && availableResolutions.isNotEmpty()) {\n            ResolutionDialog(\n                onHideDialog = { showQualityDialog = false },\n                availableResolutions = availableResolutions.sortedByDescending { it.code },\n                currentResolution = currentResolution,\n                onResolutionChange = onResolutionChange\n            )\n        }\n    }\n\n    if (showPlayModeDialog) {\n        PlayModeDialog(\n            onHideDialog = { showPlayModeDialog = false },\n            currentPlayMode = currentPlayMode,\n            hasPreloadedVideoList = hasPreloadedVideoList,\n            hasRelatedVideos = hasRelatedVideos,\n            fromSeason = fromSeason,\n            onPlayModeChange = onPlayModeChange\n        )\n    }\n}\n\n@Composable\nprivate fun PlayModeDialog(\n    modifier: Modifier = Modifier,\n    currentPlayMode: PlayMode,\n    hasPreloadedVideoList: Boolean,\n    hasRelatedVideos: Boolean,\n    fromSeason: Boolean = false,\n    onHideDialog: () -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val context = LocalContext.current\n    val availableModes = remember(hasPreloadedVideoList, hasRelatedVideos, fromSeason) {\n        PlayMode.entries.filter { mode ->\n            when (mode) {\n                PlayMode.ListOrder -> hasPreloadedVideoList && !fromSeason\n                PlayMode.ListOrderReverse -> hasPreloadedVideoList && !fromSeason\n                PlayMode.RelatedVideo -> hasRelatedVideos && !fromSeason\n                else -> true\n            }\n        }\n    }\n    val effectivePlayMode = if (currentPlayMode in availableModes) currentPlayMode else PlayMode.PartAndEpisode\n    val focusRequesters = remember(availableModes) { availableModes.associateWith { FocusRequester() } }\n    var lastInteractionTime by remember { mutableStateOf(System.currentTimeMillis()) }\n\n    fun touch() { lastInteractionTime = System.currentTimeMillis() }\n\n    LaunchedEffect(effectivePlayMode) {\n        focusRequesters[effectivePlayMode]?.requestFocus(scope)\n    }\n\n    LaunchedEffect(lastInteractionTime) {\n        val base = lastInteractionTime\n        delay(15000)\n        if (base == lastInteractionTime) onHideDialog()\n    }\n\n    Dialog(onDismissRequest = { onHideDialog() }) {\n        Surface(\n            modifier = modifier\n                .width(240.dp)\n                .heightIn(max = 300.dp),\n            color = Color.Black.copy(alpha = 0.5f),\n            shape = MaterialTheme.shapes.medium\n        ) {\n            Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {\n                Text(\n                    text = stringResource(R.string.video_player_menu_others_play_mode),\n                    color = Color.White,\n                    style = MaterialTheme.typography.titleMedium,\n                    fontSize = 18.sp\n                )\n\n                LazyColumn(\n                    modifier = Modifier.fillMaxWidth(),\n                    contentPadding = PaddingValues(top = 8.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    items(availableModes) { mode ->\n                        val selected = mode == effectivePlayMode\n                        Button(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = 8.dp)\n                                .focusRequester(focusRequesters[mode]!!),\n                            shape = ButtonDefaults.shape(MaterialTheme.shapes.medium),\n                            scale = ButtonDefaults.scale(focusedScale = 1f),\n                            colors = ButtonDefaults.colors(\n                                containerColor = if (selected) MaterialTheme.colorScheme.inverseSurface.copy(\n                                    alpha = 0.4f\n                                ) else Color.Transparent,\n                                contentColor = Color.White,\n                                focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n                                focusedContentColor = Color.Black\n                            ),\n                            onClick = { touch(); onPlayModeChange(mode); }\n                        ) {\n                            Text(\n                                modifier = Modifier.fillMaxWidth(),\n                                text = mode.getDisplayName(context),\n                                textAlign = TextAlign.Center,\n                                fontSize = 16.sp\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SpeedDialog(\n    modifier: Modifier = Modifier,\n    onHideDialog: () -> Unit,\n    speed: Float,\n    step: Float = 0.25f,\n    min: Float = 0.25f,\n    max: Float = 3f,\n    onSpeedChange: (Float) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n    // 记录最后一次交互时间，用于自动关闭\n    var lastInteractionTime by remember { mutableStateOf(System.currentTimeMillis()) }\n\n    // 每次速度变化或获得焦点按键交互时更新交互时间\n    fun touch() { lastInteractionTime = System.currentTimeMillis() }\n\n    LaunchedEffect(Unit) {\n        focusRequester.requestFocus(scope)\n    }\n\n    // 10 秒无操作自动关闭\n    LaunchedEffect(lastInteractionTime) {\n        val base = lastInteractionTime\n        delay(10000)\n        // 如果期间没有新的交互，则关闭\n        if (base == lastInteractionTime) onHideDialog()\n    }\n\n    Dialog(onDismissRequest = { onHideDialog() }) {\n        Surface(\n            modifier = modifier\n                .width(240.dp),\n            color = Color.Black.copy(alpha = 0.5f),\n            shape = MaterialTheme.shapes.medium\n        ) {\n            Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {\n                Text(\n                    text = \"播放速度\",\n                    color = Color.White,\n                    style = MaterialTheme.typography.titleMedium,\n                    fontSize = 18.sp\n                )\n\n                Column(\n                    modifier = Modifier\n                        .focusRequester(focusRequester)\n                        .focusable()\n                        .fillMaxWidth()\n                        .onPreviewKeyEvent {\n                            if (it.key == Key.DirectionUp || it.key == Key.DirectionDown || it.key == Key.DirectionLeft || it.key == Key.DirectionRight) {\n                                if (it.type == KeyEventType.KeyDown) {\n                                    touch()\n                                    var newValue = if (it.key == Key.DirectionUp || it.key == Key.DirectionRight)\n                                        speed + step\n                                    else\n                                        speed - step\n                                    if (newValue < min) newValue = min\n                                    if (newValue > max) newValue = max\n                                    onSpeedChange(newValue)\n                                }\n                            }\n                            false\n                        },\n                    horizontalAlignment = Alignment.CenterHorizontally\n                ) {\n                    Icon(\n                        imageVector = Icons.Rounded.ArrowDropUp,\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                    Text(text = \"${speed}x\", color = Color.White, fontSize = 16.sp)\n                    Icon(\n                        imageVector = Icons.Rounded.ArrowDropDown,\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RotationDialog(\n    modifier: Modifier = Modifier,\n    rotation: VideoRotation,\n    onHideDialog: () -> Unit,\n    onRotationChange: (VideoRotation) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val options = remember { VideoRotation.entries }\n    val context = LocalContext.current\n    val focusRequesters = remember { options.associateWith { FocusRequester() } }\n    var lastInteractionTime by remember { mutableStateOf(System.currentTimeMillis()) }\n\n    fun touch() { lastInteractionTime = System.currentTimeMillis() }\n\n    LaunchedEffect(rotation) {\n        focusRequesters[rotation]?.requestFocus(scope)\n    }\n\n    // 自动关闭逻辑：15秒无交互\n    LaunchedEffect(lastInteractionTime) {\n        val base = lastInteractionTime\n        delay(15000)\n        if (base == lastInteractionTime) onHideDialog()\n    }\n\n    Dialog(onDismissRequest = { onHideDialog() }) {\n        Surface(\n            modifier = modifier\n                .width(240.dp),\n            color = Color.Black.copy(alpha = 0.5f),\n            shape = MaterialTheme.shapes.medium\n        ) {\n            Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {\n                Text(\n                    text = stringResource(R.string.video_player_menu_picture_rotation),\n                    color = Color.White,\n                    style = MaterialTheme.typography.titleMedium,\n                    fontSize = 18.sp\n                )\n\n                Column {\n                    options.forEach { option ->\n                        val selected = option == rotation\n                        Button(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(top = 8.dp, start = 8.dp, end = 8.dp)\n                                .focusRequester(focusRequesters[option]!!),\n                            shape = ButtonDefaults.shape(MaterialTheme.shapes.medium),\n                            scale = ButtonDefaults.scale(focusedScale = 1f),\n                            colors = ButtonDefaults.colors(\n                                containerColor = if (selected) MaterialTheme.colorScheme.inverseSurface.copy(\n                                    alpha = 0.4f\n                                ) else Color.Transparent,\n                                contentColor = Color.White,\n                                focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n                                focusedContentColor = Color.Black\n                            ),\n                            onClick = { touch(); onRotationChange(option); }\n                        ) {\n                            Text(\n                                modifier = Modifier.fillMaxWidth(),\n                                text = option.getDisplayName(context),\n                                textAlign = TextAlign.Center,\n                                fontSize = 16.sp\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SubtitleDialog(\n    modifier: Modifier = Modifier,\n    subtitle: Subtitle,\n    availableSubtitleTracks: List<Subtitle>,\n    onHideDialog: () -> Unit,\n    onSubtitleChange: (Subtitle) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val context = LocalContext.current\n    val focusRequesters = remember { availableSubtitleTracks.map { it.id }.associateWith { FocusRequester() } }\n    var lastInteractionTime by remember { mutableStateOf(System.currentTimeMillis()) }\n\n    fun touch() { lastInteractionTime = System.currentTimeMillis() }\n\n    LaunchedEffect(subtitle) {\n        focusRequesters[subtitle.id]?.requestFocus(scope)\n    }\n\n    LaunchedEffect(lastInteractionTime) {\n        val base = lastInteractionTime\n        delay(15000)\n        if (base == lastInteractionTime) onHideDialog()\n    }\n\n    Dialog(onDismissRequest = { onHideDialog() }) {\n        Surface(\n            modifier = modifier\n                .width(240.dp),\n            color = Color.Black.copy(alpha = 0.5f),\n            shape = MaterialTheme.shapes.medium\n        ) {\n            Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {\n                Text(\n                    text = stringResource(R.string.video_player_menu_subtitle_switch),\n                    color = Color.White,\n                    style = MaterialTheme.typography.titleMedium,\n                    fontSize = 18.sp\n                )\n\n                Column {\n                    availableSubtitleTracks.forEach { option ->\n                        val selected = option == subtitle\n                        Button(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(top = 8.dp, start = 8.dp, end = 8.dp)\n                                .focusRequester(focusRequesters[option.id]!!),\n                            shape = ButtonDefaults.shape(MaterialTheme.shapes.medium),\n                            scale = ButtonDefaults.scale(focusedScale = 1f),\n                            colors = ButtonDefaults.colors(\n                                containerColor = if (selected) MaterialTheme.colorScheme.inverseSurface.copy(\n                                    alpha = 0.4f\n                                ) else Color.Transparent,\n                                contentColor = Color.White,\n                                focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n                                focusedContentColor = Color.Black\n                            ),\n                            onClick = { touch(); onSubtitleChange(option) }\n                        ) {\n                            Text(\n                                modifier = Modifier.fillMaxWidth(),\n                                text = option.langDoc,\n                                textAlign = TextAlign.Center,\n                                fontSize = 16.sp\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun LiveQualityDialog(\n    modifier: Modifier = Modifier,\n    availableLiveQualities: List<Pair<Int, String>>,\n    currentLiveQn: Int,\n    onHideDialog: () -> Unit,\n    onLiveQualityChange: (Int) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val focusRequesters = remember { availableLiveQualities.associate { it.first to FocusRequester() } }\n    var lastInteractionTime by remember { mutableStateOf(System.currentTimeMillis()) }\n\n    fun touch() { lastInteractionTime = System.currentTimeMillis() }\n\n    LaunchedEffect(currentLiveQn) {\n        focusRequesters[currentLiveQn]?.requestFocus(scope)\n    }\n\n    LaunchedEffect(lastInteractionTime) {\n        val base = lastInteractionTime\n        delay(15000)\n        if (base == lastInteractionTime) onHideDialog()\n    }\n\n    Dialog(onDismissRequest = { onHideDialog() }) {\n        Surface(\n            modifier = modifier\n                .width(240.dp),\n            color = Color.Black.copy(alpha = 0.5f),\n            shape = MaterialTheme.shapes.medium\n        ) {\n            Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {\n                Text(\n                    text = \"直播画质\",\n                    color = Color.White,\n                    style = MaterialTheme.typography.titleMedium,\n                    fontSize = 18.sp\n                )\n\n                Column {\n                    availableLiveQualities.forEach { (qn, description) ->\n                        val selected = qn == currentLiveQn\n                        Button(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(top = 8.dp, start = 8.dp, end = 8.dp)\n                                .focusRequester(focusRequesters[qn]!!),\n                            shape = ButtonDefaults.shape(MaterialTheme.shapes.medium),\n                            scale = ButtonDefaults.scale(focusedScale = 1f),\n                            colors = ButtonDefaults.colors(\n                                containerColor = if (selected) MaterialTheme.colorScheme.inverseSurface.copy(\n                                    alpha = 0.4f\n                                ) else Color.Transparent,\n                                contentColor = Color.White,\n                                focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n                                focusedContentColor = Color.Black\n                            ),\n                            onClick = { touch(); onLiveQualityChange(qn) }\n                        ) {\n                            Text(\n                                modifier = Modifier.fillMaxWidth(),\n                                text = description,\n                                textAlign = TextAlign.Center,\n                                fontSize = 16.sp\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ResolutionDialog(\n    modifier: Modifier = Modifier,\n    availableResolutions: List<Resolution>,\n    currentResolution: Resolution,\n    onHideDialog: () -> Unit,\n    onResolutionChange: (Resolution) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val context = LocalContext.current\n    val focusRequesters = remember { availableResolutions.associateWith { FocusRequester() } }\n    var lastInteractionTime by remember { mutableStateOf(System.currentTimeMillis()) }\n\n    fun touch() { lastInteractionTime = System.currentTimeMillis() }\n\n    LaunchedEffect(currentResolution) {\n        focusRequesters[currentResolution]?.requestFocus(scope)\n    }\n\n    LaunchedEffect(lastInteractionTime) {\n        val base = lastInteractionTime\n        delay(15000)\n        if (base == lastInteractionTime) onHideDialog()\n    }\n\n    Dialog(onDismissRequest = { onHideDialog() }) {\n        Surface(\n            modifier = modifier\n                .width(240.dp),\n            color = Color.Black.copy(alpha = 0.5f),\n            shape = MaterialTheme.shapes.medium\n        ) {\n            Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {\n                Text(\n                    text = \"画质\",\n                    color = Color.White,\n                    style = MaterialTheme.typography.titleMedium,\n                    fontSize = 18.sp\n                )\n\n                Column {\n                    availableResolutions.forEach { resolution ->\n                        val selected = resolution == currentResolution\n                        Button(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(top = 8.dp, start = 8.dp, end = 8.dp)\n                                .focusRequester(focusRequesters[resolution]!!),\n                            shape = ButtonDefaults.shape(MaterialTheme.shapes.medium),\n                            scale = ButtonDefaults.scale(focusedScale = 1f),\n                            colors = ButtonDefaults.colors(\n                                containerColor = if (selected) MaterialTheme.colorScheme.inverseSurface.copy(\n                                    alpha = 0.4f\n                                ) else Color.Transparent,\n                                contentColor = Color.White,\n                                focusedContainerColor = MaterialTheme.colorScheme.inverseSurface,\n                                focusedContentColor = Color.Black\n                            ),\n                            onClick = { touch(); onResolutionChange(resolution) }\n                        ) {\n                            Text(\n                                modifier = Modifier.fillMaxWidth(),\n                                text = resolution.getShortDisplayName(context),\n                                textAlign = TextAlign.Center,\n                                fontSize = 16.sp\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun Clock(\n    modifier: Modifier = Modifier,\n    hour: Int,\n    minute: Int,\n    second: Int\n) {\n    Text(\n        modifier = modifier,\n        color = Color.White,\n        fontWeight = FontWeight.Bold,\n        letterSpacing = 2.sp,\n        style = TextStyle(\n            shadow = Shadow(\n                color = Color.Black,\n                blurRadius = 4f\n            )\n        ),\n        text = buildAnnotatedString {\n            withStyle(SpanStyle(fontSize = 32.sp)) {\n                append(\"$hour\".padStart(2, '0'))\n                append(\":\")\n                append(\"$minute\".padStart(2, '0'))\n            }\n            withStyle(SpanStyle(fontSize = 18.sp)) {\n                append(\":\")\n                append(\"$second\".padStart(2, '0'))\n            }\n        }\n    )\n}\n\n@Preview\n@Composable\nprivate fun ClockPreview() {\n    val clock = Triple(12, 30, 30)\n    MaterialTheme {\n        Clock(\n            hour = clock.first,\n            minute = clock.second,\n            second = clock.third\n        )\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun ControllerVideoInfoPreview() {\n    var show by remember { mutableStateOf(true) }\n\n    val clockState = VideoPlayerClockState(hour = 12, minute = 30, second = 30)\n    CompositionLocalProvider(\n        LocalVideoPlayerVideoInfoData provides VideoPlayerVideoInfoData(\n            title = \"【A320】民航史上最佳逆袭！A320的前世今生！民航史上最佳逆袭！A320的前世今生！\",\n            partTitle = \"2023车队车手介绍分析预测 2023车队车手介绍分析预测 2023车队车手介绍分析预测\",\n            upName = \"upName\",\n            play = 1,\n            danmaku = 1,\n            pubTime = \"2025-08-05\"\n        ),\n        LocalVideoPlayerClockState provides clockState,\n        LocalVideoPlayerSeekThumbData provides VideoPlayerSeekThumbData(\n            idleIcon = \"\",\n            movingIcon = \"\"\n        )\n    ) {\n        MaterialTheme {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .background(Color.White),\n                contentAlignment = Alignment.Center\n            ) {\n                Button(onClick = { show = !show }) {\n                    Text(text = \"Switch\")\n                }\n            }\n            ControllerVideoInfo(\n                modifier = Modifier.fillMaxSize(),\n                show = show,\n                playSpeed = 1.25f,\n                onHideInfo = {},\n                onPlay = {},\n                onPause = {},\n                onPlaySpeedChange = {},\n                onOpenUpSpace = {},\n                onRefreshVideo = {},\n                onOpenDanmaku = {},\n                onHideDanmaku = {},\n                onOpenPlayList = {},\n                onOpenRelatedVideo = {},\n                onOpenSetting = {},\n                onPlayModeChange = {},\n                onRotationChange = {},\n                userActionContent = { _, _, _, _ ->\n                    // User action buttons go here\n                },\n                onSeekBack = {},\n                onSeekForward = {},\n                onSubtitleChange = {},\n                onLoadNextVideo = {}\n            )\n        }\n    }\n}\n\n\n@Preview\n@Composable\nprivate fun SpeedDialogPreview() {\n    SpeedDialog(\n        speed = 1.25f,\n        onHideDialog = {},\n        onSpeedChange = {}\n    )\n}\n\n@Preview\n@Composable\nprivate fun RotationDialogPreview() {\n    RotationDialog(\n        rotation = VideoRotation.Rotate90,\n        onHideDialog = {},\n        onRotationChange = {}\n    )\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/LiveViewerCountTip.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Group\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport androidx.compose.ui.draw.scale\n\n/**\n * 直播观看人气显示组件（左下角常驻）\n *\n * @param modifier 修饰符\n * @param show 是否显示\n * @param popularityText 人气文本，如 \"2.5万人气\"\n * @param onlineCount 高能观众文本，如 \"4333 高能观众\"\n */\n@Composable\nfun LiveViewerCountTip(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    popularityText: String,\n    onlineCount: String = \"\"\n) {\n    AnimatedVisibility(\n        visible = show,\n        enter = fadeIn(),\n        exit = fadeOut()\n    ) {\n        Box(\n            modifier = modifier.fillMaxSize()\n        ) {\n            Row(\n                modifier = Modifier\n                    .align(Alignment.BottomStart)\n                    .padding(start = 20.dp, bottom = 32.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                val contentColor = Color.White.copy(alpha = 0.6f)\n\n                Icon(\n                    modifier = Modifier.scale(0.8f),\n                    imageVector = Icons.Default.Group,\n                    contentDescription = null,\n                    tint = contentColor\n                )\n                val displayText = buildString {\n                    if (popularityText.isNotEmpty()) append(popularityText)\n                    if (onlineCount.isNotEmpty()) {\n                        if (isNotEmpty()) append(\" · \")\n                        append(onlineCount)\n                    }\n                }\n                if (displayText.isNotEmpty()) {\n                    Text(\n                        modifier = Modifier.padding(start = 4.dp),\n                        text = displayText,\n                        style = MaterialTheme.typography.titleMedium.copy(\n                            color = contentColor,\n                            fontSize = 14.sp\n                        ),\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/MenuController.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.SurfaceDefaults\nimport dev.aaa1115910.biliapi.entity.video.Subtitle\nimport dev.aaa1115910.biliapi.entity.video.SubtitleAiStatus\nimport dev.aaa1115910.biliapi.entity.video.SubtitleAiType\nimport dev.aaa1115910.biliapi.entity.video.SubtitleType\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.DanmakuType\nimport dev.aaa1115910.bv.player.entity.LiveCodec\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoAspectRatio\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.player.entity.VideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerMenuNavItem\nimport dev.aaa1115910.bv.player.entity.VideoRotation\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.ClosedCaptionMenuList\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.DanmakuMenuList\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.MenuNavList\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.OthersMenuList\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.PictureMenuList\nimport dev.aaa1115910.bv.util.requestFocus\nimport dev.aaa1115910.bv.util.swapList\n\n@Composable\nfun MenuController(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onResolutionChange: (Resolution) -> Unit = {},\n    onCodecChange: (VideoCodec) -> Unit = {},\n    onAspectRatioChange: (VideoAspectRatio) -> Unit,\n    onRotationChange: (VideoRotation) -> Unit,\n    onPlaySpeedChange: (Float) -> Unit = {},\n    onAudioChange: (Audio) -> Unit,\n    onLiveQualityChange: (Int) -> Unit = {},\n    onLiveCodecChange: (LiveCodec) -> Unit = {},\n    onDanmakuSwitchChange: (List<DanmakuType>) -> Unit,\n    onDanmakuSizeChange: (Float) -> Unit,\n    onDanmakuOpacityChange: (Float) -> Unit,\n    onDanmakuAreaChange: (Float) -> Unit,\n    onDanmakuMaskChange: (Boolean) -> Unit = {},\n    onDanmakuRollingDurationFactorChange: (Float) -> Unit,\n    onDanmakuFilterLevelChange: (Int) -> Unit = {},\n    onSubtitleChange: (Subtitle) -> Unit,\n    onSubtitleSizeChange: (TextUnit) -> Unit,\n    onSubtitleBackgroundOpacityChange: (Float) -> Unit,\n    onSubtitleBottomPadding: (Dp) -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit,\n    onDebugInfoChange: (Boolean) -> Unit = {}\n) {\n    val scope = rememberCoroutineScope()\n    val defaultFocusRequester = remember { FocusRequester() }\n\n    Box(\n        modifier = modifier.fillMaxSize(),\n        contentAlignment = Alignment.CenterEnd\n    ) {\n        AnimatedVisibility(\n            visible = show,\n            enter = expandHorizontally(),\n            exit = shrinkHorizontally()\n        ) {\n            // 在动画内容中处理焦点请求\n            LaunchedEffect(Unit) {\n                defaultFocusRequester.requestFocus(scope)\n            }\n            MenuController(\n                defaultFocusRequester = defaultFocusRequester,\n                onResolutionChange = onResolutionChange,\n                onCodecChange = onCodecChange,\n                onAspectRatioChange = onAspectRatioChange,\n                onRotationChange = onRotationChange,\n                onPlaySpeedChange = onPlaySpeedChange,\n                onAudioChange = onAudioChange,\n                onLiveQualityChange = onLiveQualityChange,\n                onLiveCodecChange = onLiveCodecChange,\n                onDanmakuSwitchChange = onDanmakuSwitchChange,\n                onDanmakuSizeChange = onDanmakuSizeChange,\n                onDanmakuOpacityChange = onDanmakuOpacityChange,\n                onDanmakuAreaChange = onDanmakuAreaChange,\n                onDanmakuMaskChange = onDanmakuMaskChange,\n                onDanmakuRollingDurationFactorChange = onDanmakuRollingDurationFactorChange,\n                onDanmakuFilterLevelChange = onDanmakuFilterLevelChange,\n                onSubtitleChange = onSubtitleChange,\n                onSubtitleSizeChange = onSubtitleSizeChange,\n                onSubtitleBackgroundOpacityChange = onSubtitleBackgroundOpacityChange,\n                onSubtitleBottomPadding = onSubtitleBottomPadding,\n                onPlayModeChange = onPlayModeChange,\n                onDebugInfoChange = onDebugInfoChange\n            )\n        }\n    }\n}\n\n@Composable\nfun MenuController(\n    modifier: Modifier = Modifier,\n    defaultFocusRequester: FocusRequester,\n    onResolutionChange: (Resolution) -> Unit = {},\n    onCodecChange: (VideoCodec) -> Unit = {},\n    onAspectRatioChange: (VideoAspectRatio) -> Unit,\n    onRotationChange: (VideoRotation) -> Unit,\n    onPlaySpeedChange: (Float) -> Unit,\n    onAudioChange: (Audio) -> Unit,\n    onLiveQualityChange: (Int) -> Unit = {},\n    onLiveCodecChange: (LiveCodec) -> Unit = {},\n    onDanmakuSwitchChange: (List<DanmakuType>) -> Unit,\n    onDanmakuSizeChange: (Float) -> Unit,\n    onDanmakuOpacityChange: (Float) -> Unit,\n    onDanmakuAreaChange: (Float) -> Unit,\n    onDanmakuMaskChange: (Boolean) -> Unit = {},\n    onDanmakuRollingDurationFactorChange: (Float) -> Unit,\n    onDanmakuFilterLevelChange: (Int) -> Unit = {},\n    onSubtitleChange: (Subtitle) -> Unit,\n    onSubtitleSizeChange: (TextUnit) -> Unit,\n    onSubtitleBackgroundOpacityChange: (Float) -> Unit,\n    onSubtitleBottomPadding: (Dp) -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit,\n    onDebugInfoChange: (Boolean) -> Unit = {}\n) {\n    var selectedNavItem by remember { mutableStateOf(VideoPlayerMenuNavItem.Picture) }\n    var focusState by remember { mutableStateOf(MenuFocusState.MenuNav) }\n\n    Surface(\n        modifier = modifier\n            .fillMaxHeight(),\n        colors = SurfaceDefaults.colors(\n            containerColor = Color.Black.copy(alpha = 0.5f)\n        )\n    ) {\n        CompositionLocalProvider(\n            LocalMenuFocusStateData provides MenuFocusStateData(\n                focusState = focusState\n            )\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.End\n            ) {\n                MenuList(\n                    selectedNavMenu = selectedNavItem,\n                    onResolutionChange = onResolutionChange,\n                    onCodecChange = onCodecChange,\n                    onPlaySpeedChange = onPlaySpeedChange,\n                    onAspectRatioChange = onAspectRatioChange,\n                    onRotationChange = onRotationChange,\n                    onAudioChange = onAudioChange,\n                    onLiveQualityChange = onLiveQualityChange,\n                    onLiveCodecChange = onLiveCodecChange,\n                    onDanmakuSwitchChange = onDanmakuSwitchChange,\n                    onDanmakuSizeChange = onDanmakuSizeChange,\n                    onDanmakuOpacityChange = onDanmakuOpacityChange,\n                    onDanmakuAreaChange = onDanmakuAreaChange,\n                    onDanmakuMaskChange = onDanmakuMaskChange,\n                    onDanmakuRollingDurationFactorChange = onDanmakuRollingDurationFactorChange,\n                    onDanmakuFilterLevelChange = onDanmakuFilterLevelChange,\n                    onFocusStateChange = { focusState = it },\n                    onSubtitleChange = onSubtitleChange,\n                    onSubtitleSizeChange = onSubtitleSizeChange,\n                    onSubtitleBackgroundOpacityChange = onSubtitleBackgroundOpacityChange,\n                    onSubtitleBottomPadding = onSubtitleBottomPadding,\n                    onPlayModeChange = onPlayModeChange,\n                    onDebugInfoChange = onDebugInfoChange\n                )\n                MenuNavList(\n                    modifier = Modifier\n                        .focusRequester(defaultFocusRequester)\n                        .onPreviewKeyEvent {\n                            if (it.type == KeyEventType.KeyUp) {\n                                if (listOf(Key.Enter, Key.DirectionCenter).contains(it.key)) {\n                                    return@onPreviewKeyEvent false\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                            if (it.key == Key.DirectionLeft) focusState = MenuFocusState.Menu\n                            false\n                        },\n                    selectedMenu = selectedNavItem,\n                    onSelectedChanged = { selectedNavItem = it },\n                    isFocusing = focusState == MenuFocusState.MenuNav\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun MenuList(\n    modifier: Modifier = Modifier,\n    selectedNavMenu: VideoPlayerMenuNavItem,\n    onResolutionChange: (Resolution) -> Unit,\n    onCodecChange: (VideoCodec) -> Unit,\n    onAspectRatioChange: (VideoAspectRatio) -> Unit,\n    onRotationChange: (VideoRotation) -> Unit,\n    onPlaySpeedChange: (Float) -> Unit,\n    onAudioChange: (Audio) -> Unit,\n    onLiveQualityChange: (Int) -> Unit = {},\n    onLiveCodecChange: (LiveCodec) -> Unit = {},\n    onDanmakuSwitchChange: (List<DanmakuType>) -> Unit,\n    onDanmakuSizeChange: (Float) -> Unit,\n    onDanmakuOpacityChange: (Float) -> Unit,\n    onDanmakuAreaChange: (Float) -> Unit,\n    onDanmakuMaskChange: (Boolean) -> Unit = {},\n    onDanmakuRollingDurationFactorChange: (Float) -> Unit,\n    onDanmakuFilterLevelChange: (Int) -> Unit = {},\n    onSubtitleChange: (Subtitle) -> Unit,\n    onSubtitleSizeChange: (TextUnit) -> Unit,\n    onSubtitleBackgroundOpacityChange: (Float) -> Unit,\n    onSubtitleBottomPadding: (Dp) -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit,\n    onDebugInfoChange: (Boolean) -> Unit = {},\n    onFocusStateChange: (MenuFocusState) -> Unit\n) {\n    Box(\n        modifier = modifier,\n        contentAlignment = Alignment.Center\n    ) {\n        when (selectedNavMenu) {\n            VideoPlayerMenuNavItem.Picture -> {\n                PictureMenuList(\n                    onResolutionChange = onResolutionChange,\n                    onCodecChange = onCodecChange,\n                    onAspectRatioChange = onAspectRatioChange,\n                    onRotationChange = onRotationChange,\n                    onPlaySpeedChange = onPlaySpeedChange,\n                    onAudioChange = onAudioChange,\n                    onLiveQualityChange = onLiveQualityChange,\n                    onLiveCodecChange = onLiveCodecChange,\n                    onFocusStateChange = onFocusStateChange\n                )\n            }\n\n            VideoPlayerMenuNavItem.Danmaku -> {\n                DanmakuMenuList(\n                    onDanmakuSwitchChange = onDanmakuSwitchChange,\n                    onDanmakuSizeChange = onDanmakuSizeChange,\n                    onDanmakuOpacityChange = onDanmakuOpacityChange,\n                    onDanmakuAreaChange = onDanmakuAreaChange,\n                    onFocusStateChange = onFocusStateChange,\n                    onDanmakuMaskChange = onDanmakuMaskChange,\n                    onDanmakuRollingDurationFactorChange = onDanmakuRollingDurationFactorChange,\n                    onDanmakuFilterLevelChange = onDanmakuFilterLevelChange\n                )\n            }\n\n            VideoPlayerMenuNavItem.ClosedCaption -> {\n                ClosedCaptionMenuList(\n                    onSubtitleChange = onSubtitleChange,\n                    onSubtitleSizeChange = onSubtitleSizeChange,\n                    onSubtitleBackgroundOpacityChange = onSubtitleBackgroundOpacityChange,\n                    onSubtitleBottomPadding = onSubtitleBottomPadding,\n                    onFocusStateChange = onFocusStateChange\n                )\n            }\n\n            VideoPlayerMenuNavItem.Others -> {\n                OthersMenuList(\n                    onPlayModeChange = onPlayModeChange,\n                    onDebugInfoChange = onDebugInfoChange,\n                    onFocusStateChange = onFocusStateChange\n                )\n            }\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nfun MenuControllerPreview() {\n\n    val defaultFocusRequester = remember { FocusRequester() }\n\n    var currentResolution by remember { mutableStateOf(Resolution.R240P) }\n    var currentCodec by remember { mutableStateOf(VideoCodec.HEVC) }\n    var currentVideoAspectRatio by remember { mutableStateOf(VideoAspectRatio.Default) }\n    var currentVideoRotation by remember { mutableStateOf(VideoRotation.Original) }\n    var currentPlaySpeed by remember { mutableFloatStateOf(1f) }\n    var currentAudio by remember { mutableStateOf(Audio.A192K) }\n\n    val currentDanmakuSwitch = remember { mutableStateListOf<DanmakuType>() }\n    var currentDanmakuSize by remember { mutableFloatStateOf(1f) }\n    var currentDanmakuOpacity by remember { mutableFloatStateOf(1f) }\n    var currentDanmakuArea by remember { mutableFloatStateOf(1f) }\n    var currentDanmakuMask by remember { mutableStateOf(false) }\n    var currentDanmakuRollingDurationFactor by remember { mutableFloatStateOf(1f) }\n\n    var currentSubtitleId by remember { mutableLongStateOf(-1L) }\n    val currentSubtitleList = remember { mutableStateListOf<Subtitle>() }\n    var currentSubtitleFontSize by remember { mutableStateOf(24.sp) }\n    var currentSubtitleBackgroundOpacity by remember { mutableFloatStateOf(0.4f) }\n    var currentSubtitleBottomPadding by remember { mutableStateOf(8.dp) }\n\n    var currentPlayMode by remember { mutableStateOf(PlayMode.PartAndEpisode) }\n\n    LaunchedEffect(Unit) {\n        currentSubtitleList.apply {\n            addAll(\n                listOf(\n                    Subtitle(\n                        id = -1,\n                        langDoc = \"关闭\",\n                        lang = \"\",\n                        url = \"\",\n                        type = SubtitleType.CC,\n                        aiType = SubtitleAiType.Normal,\n                        aiStatus = SubtitleAiStatus.None\n                    ),\n                    Subtitle(\n                        id = 1111,\n                        langDoc = \"ai-zh\",\n                        lang = \"中文（自动翻译）\",\n                        url = \"\",\n                        type = SubtitleType.CC,\n                        aiType = SubtitleAiType.Normal,\n                        aiStatus = SubtitleAiStatus.None\n                    ),\n                    Subtitle(\n                        id = 222,\n                        lang = \"zh\",\n                        langDoc = \"中文\",\n                        url = \"\",\n                        type = SubtitleType.CC,\n                        aiType = SubtitleAiType.Normal,\n                        aiStatus = SubtitleAiStatus.None\n                    ),\n                    Subtitle(\n                        id = 1333,\n                        lang = \"ai-en\",\n                        langDoc = \"English\",\n                        url = \"\",\n                        type = SubtitleType.CC,\n                        aiType = SubtitleAiType.Normal,\n                        aiStatus = SubtitleAiStatus.None\n                    )\n                )\n            )\n        }\n    }\n\n    MaterialTheme {\n        Surface(\n            colors = SurfaceDefaults.colors(\n                containerColor = Color.White\n            )\n        ) {\n            Box(modifier = Modifier.fillMaxSize()) {\n                CompositionLocalProvider(\n                    LocalVideoPlayerConfigData provides VideoPlayerConfigData(\n                        availableResolutions = Resolution.entries,\n                        availableVideoCodec = VideoCodec.entries,\n                        availableAudio = Audio.entries,\n\n                        currentResolution = currentResolution,\n                        currentVideoCodec = currentCodec,\n                        currentVideoAspectRatio = currentVideoAspectRatio,\n                        currentVideoRotation = currentVideoRotation,\n                        currentVideoSpeed = currentPlaySpeed,\n                        currentAudio = currentAudio,\n\n                        currentDanmakuEnabledList = currentDanmakuSwitch,\n                        currentDanmakuScale = currentDanmakuSize,\n                        currentDanmakuOpacity = currentDanmakuOpacity,\n                        currentDanmakuArea = currentDanmakuArea,\n                        currentDanmakuMask = currentDanmakuMask,\n                        currentDanmakuRollingDurationFactor = currentDanmakuRollingDurationFactor,\n\n                        currentSubtitleId = currentSubtitleId,\n                        availableSubtitleTracks = currentSubtitleList,\n                        currentSubtitleFontSize = currentSubtitleFontSize,\n                        currentSubtitleBackgroundOpacity = currentSubtitleBackgroundOpacity,\n                        currentSubtitleBottomPadding = currentSubtitleBottomPadding,\n\n                        currentPlayMode = currentPlayMode\n                    )\n                ) {\n                    MenuController(\n                        modifier = Modifier\n                            .align(Alignment.CenterEnd),\n                        defaultFocusRequester = defaultFocusRequester,\n                        onResolutionChange = { currentResolution = it },\n                        onCodecChange = { currentCodec = it },\n                        onAspectRatioChange = { currentVideoAspectRatio = it },\n                        onRotationChange = { currentVideoRotation = it },\n                        onPlaySpeedChange = { currentPlaySpeed = it },\n                        onAudioChange = { currentAudio = it },\n                        onDanmakuSwitchChange = {\n                            val a = currentDanmakuSwitch.toList()\n                            currentDanmakuSwitch.swapList(it)\n                            val b = currentDanmakuSwitch.toList()\n                            println(\"a=$a\")\n                            println(\"b=$b\")\n\n                        },\n                        onDanmakuSizeChange = { currentDanmakuSize = it },\n                        onDanmakuOpacityChange = { currentDanmakuOpacity = it },\n                        onDanmakuAreaChange = { currentDanmakuArea = it },\n                        onDanmakuMaskChange = { currentDanmakuMask = it },\n                        onDanmakuRollingDurationFactorChange = { currentDanmakuRollingDurationFactor = it },\n                        onSubtitleChange = { currentSubtitleId = it.id },\n                        onSubtitleSizeChange = { currentSubtitleFontSize = it },\n                        onSubtitleBackgroundOpacityChange = {\n                            currentSubtitleBackgroundOpacity = it\n                        },\n                        onSubtitleBottomPadding = { currentSubtitleBottomPadding = it },\n                        onPlayModeChange = { currentPlayMode = it }\n                    )\n                }\n            }\n        }\n    }\n}\n\nenum class MenuFocusState {\n    MenuNav, Menu, Items\n}\n\ndata class MenuFocusStateData(\n    val focusState: MenuFocusState = MenuFocusState.MenuNav\n)\n\nval LocalMenuFocusStateData = compositionLocalOf { MenuFocusStateData() }\n"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/OnlineViewerCountTip.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Person\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\n\n/**\n * 在线观看人数显示组件\n *\n * @param modifier 修饰符\n * @param show 是否显示\n * @param count 在线人数\n */\n@Composable\nfun OnlineViewerCountTip(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    count: String\n) {\n    AnimatedVisibility(\n        visible = show,\n        enter = fadeIn(),\n        exit = fadeOut()\n    ) {\n        Box(\n            modifier = modifier.fillMaxSize()\n        ) {\n            Row(\n                modifier = Modifier\n                    .align(Alignment.BottomStart)\n                    .padding(start = 20.dp, bottom = 32.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                val color = Color.White.copy(alpha = 0.55f)\n\n                Icon(\n                    modifier = Modifier.scale(0.8f),\n                    imageVector = Icons.Default.Person,\n                    contentDescription = null,\n                    tint = color\n                )\n                Text(\n                    modifier = Modifier.padding(start = 2.dp),\n                    text = \"$count 人在看\",\n                    style = MaterialTheme.typography.titleMedium.copy(\n                        color = color,\n                        fontSize = 14.sp,\n                        fontWeight = FontWeight.Normal\n                    ),\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/PlayStateTips.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport android.graphics.BitmapFactory\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Pause\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.TransformOrigin\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.SurfaceDefaults\nimport androidx.tv.material3.Text\nimport androidx.tv.material3.darkColorScheme\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerPaymentData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerStateData\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport qrcode.QRCode\nimport qrcode.color.DefaultColorFunction\nimport java.io.ByteArrayInputStream\nimport java.io.ByteArrayOutputStream\n\n@Composable\nfun PlayStateTips(\n    modifier: Modifier = Modifier,\n    canShowPause: Boolean = true\n) {\n    val videoPlayerStateData = LocalVideoPlayerStateData.current\n    val videoPlayerPaymentData = LocalVideoPlayerPaymentData.current\n\n    Box(\n        modifier = modifier.fillMaxSize()\n    ) {\n        if (!videoPlayerStateData.isPlaying && !videoPlayerStateData.isBuffering && !videoPlayerStateData.isError && canShowPause) {\n            PauseIcon(\n                modifier = Modifier\n                    .align(Alignment.BottomEnd)\n                    .padding(20.dp)\n            )\n        }\n        if (videoPlayerStateData.isBuffering && !videoPlayerStateData.isError) {\n            BufferingTip(\n                modifier = Modifier\n                    .align(Alignment.Center),\n                speed = \"\"\n            )\n        }\n        if (videoPlayerStateData.isError) {\n            PlayErrorTip(\n                modifier = Modifier.align(Alignment.Center),\n                exception = videoPlayerStateData.exception!!\n            )\n        }\n        if (videoPlayerPaymentData.needPay && videoPlayerPaymentData.epid > 0) {\n            PaidRequireTip(\n                modifier = Modifier\n                    .align(Alignment.BottomEnd)\n                    .padding(20.dp)\n                    .graphicsLayer {\n                        scaleX = 0.5f\n                        scaleY = 0.5f\n                        transformOrigin = TransformOrigin(1f, 1f)\n                    },\n                epid = videoPlayerPaymentData.epid\n            )\n        }\n    }\n}\n\n@Composable\nfun PauseIcon(\n    modifier: Modifier = Modifier,\n) {\n    Surface(\n        modifier = modifier,\n        colors = SurfaceDefaults.colors(\n            containerColor = Color.Black.copy(0.5f)\n        ),\n        shape = MaterialTheme.shapes.medium\n    ) {\n        Icon(\n            modifier = Modifier\n                .padding(12.dp, 4.dp)\n                .size(46.dp),\n            imageVector = Icons.Rounded.Pause,\n            contentDescription = null,\n            tint = Color.White\n        )\n    }\n}\n\n@Composable\nfun BufferingTip(\n    modifier: Modifier = Modifier,\n    speed: String\n) {\n    Surface(\n        modifier = modifier,\n        colors = SurfaceDefaults.colors(\n            containerColor = Color.Black.copy(0.5f)\n        ),\n        shape = MaterialTheme.shapes.medium\n    ) {\n        Row(\n            modifier = Modifier.padding(16.dp, 8.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            CircularProgressIndicator(\n                modifier = Modifier\n                    .size(36.dp)\n                    .padding(8.dp),\n                color = Color.White,\n                strokeWidth = 2.dp\n            )\n            Text(\n                modifier = Modifier,\n                text = \"缓冲中...$speed\",\n                fontSize = 22.sp\n            )\n        }\n    }\n}\n\n@Composable\nfun PlayErrorTip(\n    modifier: Modifier = Modifier,\n    exception: Exception\n) {\n    Surface(\n        modifier = modifier,\n        colors = SurfaceDefaults.colors(\n            containerColor = Color.Black.copy(0.5f)\n        ),\n        shape = MaterialTheme.shapes.medium\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp, 8.dp),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Text(\n                text = \"播放器正在抽风\",\n                style = MaterialTheme.typography.titleLarge\n            )\n            Text(text = \" _(:з」∠)_\")\n            Spacer(modifier = Modifier.height(12.dp))\n            Text(text = \"错误信息：${exception.message}\")\n        }\n    }\n}\n\n@Composable\nfun PaidRequireTip(\n    modifier: Modifier = Modifier,\n    epid: Int,\n) {\n    val scope = rememberCoroutineScope()\n    var qrImage by remember { mutableStateOf<ImageBitmap?>(null) }\n    LaunchedEffect(Unit) {\n        scope.launch(Dispatchers.IO) {\n            val output = ByteArrayOutputStream()\n            val url = \"https://b23.tv/ep$epid\"\n            QRCode(\n                data = url,\n                colorFn = DefaultColorFunction(\n                    foreground = android.graphics.Color.WHITE,\n                    background = android.graphics.Color.TRANSPARENT\n                )\n            )\n                .render()\n                .writeImage(output)\n            val input = ByteArrayInputStream(output.toByteArray())\n            val newQrImage = BitmapFactory.decodeStream(input).asImageBitmap()\n            withContext(Dispatchers.Main) {\n                qrImage = newQrImage\n            }\n        }\n    }\n    Surface(\n        modifier = modifier,\n        colors = SurfaceDefaults.colors(\n            containerColor = Color.Black.copy(0.5f)\n        ),\n        shape = MaterialTheme.shapes.medium\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp, 8.dp),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Text(\n                text = \"当前为试看片段，请购买影片\",\n                style = MaterialTheme.typography.titleLarge\n            )\n            // TODO 使用颜文字显示影片价格\n            // Text(text = \"(・∀・)つ㊿\")\n            Spacer(modifier = Modifier.height(12.dp))\n            AnimatedVisibility(visible = qrImage != null) {\n                Image(bitmap = qrImage!!, contentDescription = \"EP$epid QR Code\")\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun PauseIconPreview() {\n    MaterialTheme(\n        colorScheme = darkColorScheme()\n    ) {\n        Box(modifier = Modifier.padding(10.dp)) {\n            PauseIcon()\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun BufferingTipPreview() {\n    MaterialTheme(\n        colorScheme = darkColorScheme()\n    ) {\n        BufferingTip(\n            modifier = Modifier.padding(10.dp),\n            speed = \"\"\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun PlayErrorTipPreview() {\n    MaterialTheme(\n        colorScheme = darkColorScheme()\n    ) {\n        PlayErrorTip(exception = Exception(\"This is a test exception.\"))\n    }\n}\n\n@Preview\n@Composable\nprivate fun PaidRequireTipPreview() {\n    MaterialTheme(\n        colorScheme = darkColorScheme()\n    ) {\n        PaidRequireTip(epid = 752900)\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/SeekController.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.tooling.preview.PreviewParameter\nimport androidx.compose.ui.tooling.preview.PreviewParameterProvider\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport dev.aaa1115910.biliapi.entity.video.VideoShot\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekState\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekThumbData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerVideoShotData\nimport dev.aaa1115910.bv.player.seekbar.SeekMoveState\nimport dev.aaa1115910.bv.player.tv.VideoSeekBar\n\n@Composable\nfun SeekController(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    goTime: Long,\n    moveState: SeekMoveState,\n) {\n    val videoPlayerVideoShotData = LocalVideoPlayerVideoShotData.current\n    val videoPlayerSeekState = LocalVideoPlayerSeekState.current\n    val videoPlayerSeekThumbData = LocalVideoPlayerSeekThumbData.current\n\n    Box(\n        modifier = modifier.fillMaxSize()\n    ) {\n        AnimatedVisibility(\n            modifier = Modifier.align(Alignment.BottomCenter),\n            visible = show,\n            enter = expandVertically(),\n            exit = shrinkVertically(),\n            label = \"SeekControllerVisible\"\n        ) {\n            SeekController(\n                duration = videoPlayerSeekState.duration,\n                position = goTime,\n                moveState = moveState,\n                idleIcon = videoPlayerSeekThumbData.idleIcon,\n                movingIcon = videoPlayerSeekThumbData.movingIcon,\n                videoShot = videoPlayerVideoShotData.videoShot\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun SeekController(\n    modifier: Modifier = Modifier,\n    duration: Long,\n    position: Long,\n    moveState: SeekMoveState,\n    idleIcon: String,\n    movingIcon: String,\n    videoShot: VideoShot? = null\n) {\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(8.dp)\n    ) {\n        if (videoShot != null) {\n            VideoShot(\n                modifier = Modifier\n                    .padding(horizontal = 48.dp),\n                videoShot = videoShot,\n                position = position,\n                duration = duration,\n                coercedOffset = (-24).dp\n            )\n        }\n\n        Column(\n            modifier = Modifier\n                .background(\n                    Brush.verticalGradient(\n                        colors = listOf(\n                            Color.Transparent,\n                            Color.Black.copy(alpha = 0.5f)\n                        )\n                    )),\n            verticalArrangement = Arrangement.Bottom\n        ) {\n            VideoSeekBar(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 24.dp)\n                    .padding(top = 8.dp, bottom = 16.dp),\n                duration = duration,\n                position = position,\n                bufferedPercentage = 1,\n                moveState = moveState,\n                idleIcon = idleIcon,\n                movingIcon = movingIcon,\n                showPosition = true,\n                strokeWidth = 6.dp\n            )\n        }\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun VideoProgressSeekPreview(@PreviewParameter(VideoProgressProvider::class) data: Pair<Long, Long>) {\n    MaterialTheme {\n        SeekController(\n            modifier = Modifier\n                .background(Color.White),\n            duration = data.first,\n            position = data.second,\n            moveState = SeekMoveState.Idle,\n            idleIcon = \"\",\n            movingIcon = \"\",\n            videoShot = VideoShot(\n                times = emptyList(),\n                imageCountX = 0,\n                imageCountY = 0,\n                imageWidth = 0,\n                imageHeight = 0,\n                images = emptyList()\n            )\n        )\n    }\n}\n\nprivate class VideoProgressProvider : PreviewParameterProvider<Pair<Long, Long>> {\n    override val values = sequenceOf(\n        Pair(1234_000L, 0L),\n        Pair(1234_000L, 234_000L),\n        Pair(1234_000L, 555_000L),\n        Pair(1234_000L, 999_000L),\n        Pair(1234_000L, 1234_000L),\n    )\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/SkipTip.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.CornerSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.SurfaceDefaults\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.bv.player.entity.DefaultStartPosition\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerHistoryData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerPaymentData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerStateData\nimport dev.aaa1115910.bv.util.formatHourMinSec\n\n// TODO 跳转历史记录\n@Composable\nfun BackToHistoryTip(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    time: String\n) {\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val text = if (videoPlayerConfigData.defaultStartPosition == DefaultStartPosition.History) {\n        \"点击确认键跳转视频开头\"\n    } else {\n        \"上次看到 $time 点击确认键跳转\"\n    }\n    SkipTip(\n        modifier = modifier,\n        show = show,\n        text = text\n    )\n}\n\n// TODO 跳过片头\n@Composable\nfun SkipOpTip(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    text: String = \"即将跳过片头\"\n) {\n    SkipTip(\n        modifier = modifier,\n        show = show,\n        text = text\n    )\n}\n\n// TODO 跳过片尾\n@Composable\nfun SkipEdTip(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    text: String = \"即将跳过片尾\"\n) {\n    SkipTip(\n        modifier = modifier,\n        show = show,\n        text = text\n    )\n}\n\n@Composable\nfun SkipTip(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    text: String,\n    align: Alignment = Alignment.BottomStart\n) {\n    AnimatedVisibility(\n        visible = show,\n        enter = expandHorizontally(),\n        exit = shrinkHorizontally()\n    ) {\n        Box(\n            modifier = modifier.fillMaxSize()\n        ) {\n            Surface(\n                modifier = modifier\n                    .align(align)\n                    .padding(bottom = 32.dp),\n                colors = SurfaceDefaults.colors(\n                    containerColor = Color.Black.copy(alpha = 0.6f)\n                ),\n                shape = if (align == Alignment.BottomStart) {\n                    MaterialTheme.shapes.medium.copy(\n                        topStart = CornerSize(0.dp), bottomStart = CornerSize(0.dp)\n                    ) \n                } else {\n                    MaterialTheme.shapes.medium.copy(\n                        topEnd = CornerSize(0.dp), bottomEnd = CornerSize(0.dp)\n                    )\n                }\n            ) {\n                Text(\n                    modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),\n                    text = text,\n                    style = MaterialTheme.typography.titleLarge\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun SkipTips(\n    modifier: Modifier = Modifier,\n    showSkipOp: Boolean = false,\n    showSkipEd: Boolean = false,\n) {\n    val videoPlayerHistoryData = LocalVideoPlayerHistoryData.current\n    val videoPlayerStateData = LocalVideoPlayerStateData.current\n    val videoPlayerPaymentData = LocalVideoPlayerPaymentData.current\n\n    Box(modifier = modifier.fillMaxSize()) {\n        BackToHistoryTip(\n            modifier = Modifier\n                .align(Alignment.BottomStart)\n                .padding(bottom = 36.dp),\n            show = videoPlayerStateData.showBackToHistory,\n            time = videoPlayerHistoryData.lastPlayed.toLong().formatHourMinSec()\n        )\n        SkipTip(\n            modifier = Modifier\n                .align(Alignment.BottomStart)\n                .padding(bottom = 12.dp),\n            show = videoPlayerPaymentData.showPreviewTip,\n            text = \"视频需付费，当前为试看片段\"\n        )\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/UserActionContent.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\n\nenum class UserActionKey {\n    Like,\n    Favorite,\n    Coin,\n    ToView\n}\n\ntypealias UserActionContent = @Composable (\n    modifier: Modifier,\n    focusMap: Map<UserActionKey, FocusRequester>,\n    onFocus: (UserActionKey) -> Unit,\n    onPauseAutoHide: (Boolean) -> Unit\n) -> Unit\n\nval EmptyUserActionContent: UserActionContent = { _, _, _, _ -> }"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/VideoListController.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Color.Companion\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.tv.material3.ClickableSurfaceDefaults\nimport androidx.tv.material3.LocalContentColor\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Surface\nimport androidx.tv.material3.SurfaceDefaults\nimport androidx.tv.material3.Text\nimport coil.compose.AsyncImage\nimport dev.aaa1115910.bv.util.ImageSize\nimport dev.aaa1115910.bv.util.resizedImageUrl\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.VideoListItem\nimport dev.aaa1115910.bv.player.entity.VideoListItemData\nimport dev.aaa1115910.bv.player.entity.VideoListPart\nimport dev.aaa1115910.bv.player.entity.VideoListPgcEpisode\nimport dev.aaa1115910.bv.player.entity.VideoListUgcEpisode\nimport dev.aaa1115910.bv.player.entity.VideoListUgcEpisodeTitle\nimport dev.aaa1115910.bv.util.formatHourMinSec\nimport dev.aaa1115910.bv.util.formatPubTimeString\nimport dev.aaa1115910.bv.util.requestFocus\nimport java.util.Date\n\n@Composable\nfun VideoListController(\n    modifier: Modifier = Modifier,\n    show: Boolean,\n    onPlayNewVideo: (VideoListItem) -> Unit\n) {\n    val scope = rememberCoroutineScope()\n    val listState = rememberLazyListState()\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val focusRequester = remember { FocusRequester() }\n    val videoListContainsUgcEpisode by remember {\n        derivedStateOf {\n            videoPlayerConfigData.availableVideoList.any { it is VideoListUgcEpisode }\n        }\n    }\n\n    Box {\n        AnimatedVisibility(\n            visible = show,\n            enter = expandHorizontally(),\n            exit = shrinkHorizontally()\n        ) {\n            // 在动画内容中处理滚动和焦点请求\n            LaunchedEffect(Unit) {\n                val currentIndex = videoPlayerConfigData.availableVideoList\n                    .indexOfFirst {\n                        when (it) {\n                            is VideoListItemData -> it.cid == videoPlayerConfigData.currentVideoCid\n                            else -> false\n                        }\n                    }\n                if (currentIndex >= 0 && currentIndex < videoPlayerConfigData.availableVideoList.size) {\n                    listState.scrollToItem(currentIndex)\n                }\n                focusRequester.requestFocus(scope)\n            }\n            Surface(\n                modifier = modifier,\n                colors = SurfaceDefaults.colors(\n                    containerColor = Color.Black.copy(alpha = 0.5f)\n                )\n            ) {\n                Box(\n                    modifier = Modifier\n                        .padding(horizontal = 16.dp)\n                        .width(300.dp)\n                        .fillMaxHeight(),\n                    contentAlignment = Alignment.Center\n                ) {\n                    LazyColumn(\n                        state = listState,\n                        verticalArrangement = Arrangement.spacedBy(8.dp),\n                        contentPadding = PaddingValues(vertical = 80.dp)\n                    ) {\n                        items(items = videoPlayerConfigData.availableVideoList) { video ->\n                            when (video) {\n                                is VideoListPart -> {\n                                    val isSelected =\n                                        video.cid == videoPlayerConfigData.currentVideoCid\n                                    val itemModifier = if (isSelected) {\n                                        Modifier.focusRequester(focusRequester)\n                                    } else {\n                                        Modifier\n                                    }\n                                    VideoListCardItem(\n                                        modifier = itemModifier.padding(start = if (videoListContainsUgcEpisode) 12.dp else 0.dp),\n                                        title = (\" - \".takeIf { videoListContainsUgcEpisode }\n                                            ?: \"\") + \"P${video.index + 1} ${if (video.partTitle.isNotEmpty()) video.partTitle else video.title}\",\n                                        cover = video.cover,\n                                        duration = video.duration,\n                                        pubDate = video.pubDate,\n                                        isSelected = isSelected,\n                                        onClick = { if (!isSelected) onPlayNewVideo(video) }\n                                    )\n                                }\n\n                                is VideoListUgcEpisode -> {\n                                    val isSelected =\n                                        video.cid == videoPlayerConfigData.currentVideoCid\n                                    val itemModifier = if (isSelected) {\n                                        Modifier.focusRequester(focusRequester)\n                                    } else {\n                                        Modifier\n                                    }\n                                    VideoListCardItem(\n                                        modifier = itemModifier,\n                                        title = \"EP${video.index + 1} ${if (video.partTitle.isNotEmpty()) video.partTitle else video.title}\",\n                                        cover = video.cover,\n                                        duration = video.duration,\n                                        pubDate = video.pubDate,\n                                        isSelected = isSelected,\n                                        onClick = { if (!isSelected) onPlayNewVideo(video) }\n                                    )\n                                }\n\n                                is VideoListPgcEpisode -> {\n                                    val isSelected =\n                                        video.cid == videoPlayerConfigData.currentVideoCid\n                                    val itemModifier = if (isSelected) {\n                                        Modifier.focusRequester(focusRequester)\n                                    } else {\n                                        Modifier\n                                    }\n                                    VideoListCardItem(\n                                        modifier = itemModifier,\n                                        title = video.partTitle,\n                                        cover = video.cover,\n                                        duration = video.duration,\n                                        pubDate = video.pubDate,\n                                        isSelected = isSelected,\n                                        onClick = { if (!isSelected) onPlayNewVideo(video) }\n                                    )\n                                }\n\n                                is VideoListUgcEpisodeTitle -> {\n                                    Text(\n                                        modifier = Modifier.padding(\n                                            top = 12.dp,\n                                            bottom = 0.dp,\n                                        ),\n                                        text = \"- EP${video.index + 1} ${video.title}\",\n                                        style = MaterialTheme.typography.titleMedium\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun VideoListCardItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    cover: String,\n    duration: Int,\n    pubDate: Long,\n    isSelected: Boolean,\n    onClick: () -> Unit\n) {\n    var hasFocus by remember { mutableStateOf(false) }\n    val hasCover = cover.isNotBlank()\n\n    Surface(\n        modifier = modifier.onFocusChanged { hasFocus = it.hasFocus },\n        colors = ClickableSurfaceDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.09f),\n            focusedContainerColor = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.1f)\n        ),\n        scale = ClickableSurfaceDefaults.scale(scale = 1f, focusedScale = 1f),\n        shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.small),\n        onClick = onClick\n    ) {\n        val borderStroke = when {\n            hasFocus -> BorderStroke(2.dp, MaterialTheme.colorScheme.onBackground)\n            isSelected -> BorderStroke(2.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.6f))\n            else -> null\n        }\n        Box(\n            modifier = Modifier\n                .fillMaxWidth()\n                .height(76.dp)\n                .then(\n                    if (borderStroke != null) Modifier.border(\n                        border = borderStroke,\n                        shape = MaterialTheme.shapes.small\n                    ) else Modifier\n                )\n        ) {\n            Row(\n                modifier = Modifier.fillMaxSize()\n            ) {\n                if (hasCover) {\n                    AsyncImage(\n                        model = cover.resizedImageUrl(ImageSize.UgcEpisodeCover),\n                        contentDescription = null,\n                        contentScale = ContentScale.Crop,\n                        modifier = Modifier\n                            .fillMaxHeight()\n                            .aspectRatio(4f / 3f)\n                            .clip(RoundedCornerShape(topStart = 4.dp, bottomStart = 4.dp))\n                    )\n                }\n                Column(\n                    modifier = Modifier\n                        .fillMaxHeight()\n                        .weight(1f)\n                        .padding(horizontal = 8.dp, vertical = 6.dp),\n                    verticalArrangement = Arrangement.SpaceBetween\n                ) {\n                    Text(\n                        text = title,\n                        maxLines = 2,\n                        overflow = TextOverflow.Ellipsis,\n                        style = MaterialTheme.typography.bodyMedium\n                    )\n                    Row(\n                        modifier = Modifier.padding(top = 2.dp),\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        if (duration > 0) {\n                            Text(\n                                text = (duration * 1000L).formatHourMinSec(),\n                                fontSize = 11.sp,\n                                color = LocalContentColor.current.copy(alpha = 0.9f),\n                                maxLines = 1\n                            )\n                        }\n                        if (pubDate > 0L) {\n                            Text(\n                                text = Date(pubDate * 1000L).formatPubTimeString(),\n                                fontSize = 11.sp,\n                                color = LocalContentColor.current.copy(alpha = 0.9f),\n                                maxLines = 1\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/VideoPlayerController.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\nimport dev.aaa1115910.biliapi.entity.video.Subtitle\nimport dev.aaa1115910.bv.player.AbstractVideoPlayer\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.DanmakuType\nimport dev.aaa1115910.bv.player.entity.LiveCodec\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerDebugInfoData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerSeekState\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerStateData\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerVideoInfoData\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoAspectRatio\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.player.entity.VideoListItem\nimport dev.aaa1115910.bv.player.entity.VideoRotation\nimport dev.aaa1115910.bv.player.seekbar.SeekMoveState\nimport dev.aaa1115910.bv.player.shared.R\nimport dev.aaa1115910.bv.util.fInfo\nimport dev.aaa1115910.bv.util.toast\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n@Composable\nfun VideoPlayerController(\n    modifier: Modifier = Modifier,\n    videoPlayer: AbstractVideoPlayer,\n    playerSeekForwardStep: Int = 10,\n    playerSeekBackwardStep: Int = 5,\n    showBottomProgressBar: Boolean = false,\n\n    showRelatedVideos: Boolean = false,\n    onToggleRelatedVideos: (Boolean) -> Unit,\n    registerShowInfoProvider: ((() -> Boolean) -> Unit) = {},\n    onViewerCountTipCanShowChanged: (Boolean) -> Unit = {},\n    viewerCountText: String = \"\",\n\n    //player events\n    onPlay: () -> Unit,\n    onPause: () -> Unit,\n    onExit: () -> Unit,\n    onGoTime: (time: Long) -> Unit,\n    onBackToHistory: () -> Unit,\n    onPlayNewVideo: (VideoListItem) -> Unit,\n\n    onOpenUpSpace: () -> Unit,\n    onRefreshVideo: () -> Unit,\n    onOpenDanmaku: () -> Unit,\n    onHideDanmaku: () -> Unit,\n    onPlayModeChange: (PlayMode) -> Unit,\n    userActionContent: UserActionContent,\n\n    //menu events\n    onResolutionChange: (Resolution) -> Unit,\n    onCodecChange: (VideoCodec) -> Unit,\n    onAspectRatioChange: (VideoAspectRatio) -> Unit,\n    onRotationChange: (VideoRotation) -> Unit,\n    onPlaySpeedChange: (Float) -> Unit,\n    onAudioChange: (Audio) -> Unit,\n    onLiveQualityChange: (Int) -> Unit = {},\n    onLiveCodecChange: (LiveCodec) -> Unit = {},\n    onDanmakuSwitchChange: (List<DanmakuType>) -> Unit,\n    onDanmakuSizeChange: (Float) -> Unit,\n    onDanmakuOpacityChange: (Float) -> Unit,\n    onDanmakuAreaChange: (Float) -> Unit,\n    onDanmakuMaskChange: (Boolean) -> Unit,\n    onDanmakuRollingDurationFactorChange: (Float) -> Unit,\n    onDanmakuFilterLevelChange: (Int) -> Unit = {},\n    onSubtitleChange: (Subtitle) -> Unit,\n    onSubtitleSizeChange: (TextUnit) -> Unit,\n    onSubtitleBackgroundOpacityChange: (Float) -> Unit,\n    onSubtitleBottomPadding: (Dp) -> Unit,\n    onLoadNextVideo: (Boolean) -> Unit,\n    onDebugInfoChange: (Boolean) -> Unit = {},\n\n    onRequestFocus: () -> Unit,\n    onShowComment: () -> Unit = {},\n    onShowDescription: () -> Unit = {},\n    content: @Composable BoxScope.() -> Unit\n) {\n    val context = LocalContext.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val videoPlayerSeekState = LocalVideoPlayerSeekState.current\n    val videoPlayerStateData = LocalVideoPlayerStateData.current\n    val videoPlayerDebugInfoData = LocalVideoPlayerDebugInfoData.current\n    val videoPlayerVideoInfoData = LocalVideoPlayerVideoInfoData.current\n    val logger = KotlinLogging.logger {}\n    val scope = rememberCoroutineScope()\n\n    var showListController by remember { mutableStateOf(false) }\n    var showMenuController by remember { mutableStateOf(false) }\n    var showSeekController by remember { mutableStateOf(false) }\n    var showInfo by remember { mutableStateOf(false) }\n    val showClickableControllers by remember { derivedStateOf { showInfo || showListController || showMenuController } }\n\n    var lastPressBack by remember { mutableLongStateOf(0L) }\n    var lastPressDown by remember { mutableLongStateOf(0L) }\n    var longPressDownTriggered by remember { mutableStateOf(false) }\n    var hasFocus by remember { mutableStateOf(false) }\n    // 长按加速播放状态\n    var isLongPressSpeedUp by remember { mutableStateOf(false) }\n    var savedSpeedBeforeLongPress by remember { mutableStateOf(1f) }\n\n    var goTime by remember { mutableLongStateOf(0L) }\n    var seekChangeCount by remember { mutableIntStateOf(0) }\n    var lastSeekChangeTime by remember { mutableLongStateOf(0L) }\n    var moveState by remember { mutableStateOf(SeekMoveState.Idle) }\n\n    // 使用协程Job来替代CountDownTimer以确保线程安全\n    var hideVideoInfoJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }\n    var autoSeekConfirmJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }\n    var doublePressDownJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }\n\n    val openSeekController = {\n        if (!videoPlayerConfigData.isLive) {\n            if (!showSeekController) goTime = videoPlayerSeekState.position\n            showSeekController = true\n            showInfo = false\n        }\n    }\n\n    val resetAutoSeekConfirmTimer = {\n        autoSeekConfirmJob?.cancel()\n        if (showSeekController) {\n            autoSeekConfirmJob = scope.launch {\n                delay(1000)\n                if (showSeekController) {\n                    onGoTime(goTime)\n                    if (!videoPlayer.isPlaying) onPlay()\n                    withContext(Dispatchers.Main) {\n                        moveState = SeekMoveState.Idle\n                        showSeekController = false\n                    }\n                }\n            }\n        }\n    }\n\n    val calCoefficient = {\n        if (System.currentTimeMillis() - lastSeekChangeTime < 200) {\n            seekChangeCount++\n            seekChangeCount / 5\n        } else {\n            seekChangeCount = 0\n            0\n        }\n    }\n\n    val onTimeForward = {\n        if (!videoPlayerConfigData.isLive) {\n            val baseTime = playerSeekForwardStep * 1000L // 转换为毫秒\n            val targetTime = goTime + (baseTime + calCoefficient() * 5000)\n            val duration = videoPlayerSeekState.duration\n            goTime = if (targetTime > duration) duration else targetTime\n            lastSeekChangeTime = System.currentTimeMillis()\n            moveState = SeekMoveState.Forward\n            resetAutoSeekConfirmTimer()\n            logger.info { \"onTimeForward: [current=${videoPlayer.currentPosition}, goTime=$goTime]\" }\n        }\n    }\n    val onTimeBack = {\n        if (!videoPlayerConfigData.isLive) {\n            val baseTime = playerSeekBackwardStep * 1000L // 转换为毫秒\n            val targetTime = goTime - (baseTime + calCoefficient() * 5000)\n            goTime = if (targetTime < 0) 0 else targetTime\n            lastSeekChangeTime = System.currentTimeMillis()\n            moveState = SeekMoveState.Backward\n            resetAutoSeekConfirmTimer()\n            logger.info { \"onTimeBack: [current=${videoPlayer.currentPosition}, goTime=$goTime]\" }\n        }\n    }\n\n    // 对外暴露 showInfo\n    LaunchedEffect(Unit) { registerShowInfoProvider { showInfo } }\n    LaunchedEffect(showInfo, showSeekController, showListController) {\n        onViewerCountTipCanShowChanged(!showInfo && !showSeekController && !showListController)\n    }\n\n    Box(\n        modifier = modifier\n            .background(Color.Black)\n            .onFocusChanged { hasFocus = it.hasFocus }\n            .focusable()\n            //.ifElse(hasFocus, Modifier.border(2.dp, Color.Yellow))\n            .onPreviewKeyEvent {\n\n                if (showClickableControllers || showRelatedVideos) {\n                    if (listOf(Key.Back, Key.Menu).contains(it.key)) {\n                        if (it.type == KeyEventType.KeyUp) {\n                            logger.fInfo { \"[${it.key}] hide all controllers\" }\n                            scope.launch(Dispatchers.Main) {\n                                showInfo = false\n                                showMenuController = false\n                                showListController = false\n                                showSeekController = false\n                                onToggleRelatedVideos(false)\n                            }\n                        }\n                        onRequestFocus()\n                        return@onPreviewKeyEvent true\n                    }\n                    return@onPreviewKeyEvent false\n                }\n\n                if (showSeekController) {\n                    if (listOf(\n                            Key.Back,\n                            Key.Menu,\n                            Key.DirectionDown,\n                            Key.DirectionUp\n                        ).contains(it.key)\n                    ) {\n                        if (it.type != KeyEventType.KeyDown) {\n                            scope.launch(Dispatchers.Main) {\n                                showSeekController = false\n                            }\n                        }\n                        onRequestFocus()\n                        return@onPreviewKeyEvent true\n                    }\n                }\n\n                when (it.key) {\n                    Key.DirectionCenter, Key.Enter, Key.Spacebar -> {\n                        @Suppress(\"KotlinConstantConditions\")\n                        if (!showClickableControllers && videoPlayerStateData.showBackToHistory) {\n                            if (it.type == KeyEventType.KeyDown) return@onPreviewKeyEvent true\n                            onBackToHistory()\n                            return@onPreviewKeyEvent true\n                        }\n\n                        if (showSeekController) {\n                            if (it.type == KeyEventType.KeyDown) return@onPreviewKeyEvent true\n                            onGoTime(goTime)\n                            if (!videoPlayer.isPlaying) onPlay()\n                            scope.launch(Dispatchers.Main) {\n                                moveState = SeekMoveState.Idle\n                                showSeekController = false\n                            }\n                            return@onPreviewKeyEvent true\n                        }\n\n                        if (it.nativeKeyEvent.isLongPress) {\n                            logger.fInfo { \"[${it.key}] long press\" }\n                            if (videoPlayerConfigData.longPressAction == 1) {\n                                // 加速播放模式\n                                if (!isLongPressSpeedUp) {\n                                    savedSpeedBeforeLongPress = videoPlayer.speed\n                                    videoPlayer.speed = videoPlayerConfigData.longPressSpeed\n                                    isLongPressSpeedUp = true\n                                    logger.fInfo { \"Long press speed up: ${videoPlayerConfigData.longPressSpeed}x\" }\n                                }\n                            } else {\n                                scope.launch(Dispatchers.Main) {\n                                    showMenuController = true\n                                }\n                            }\n                            return@onPreviewKeyEvent true\n                        }\n\n                        logger.fInfo { \"[${it.key}] short press\" }\n                        if (it.type == KeyEventType.KeyDown) return@onPreviewKeyEvent true\n                        // 长按加速松手恢复\n                        if (isLongPressSpeedUp) {\n                            videoPlayer.speed = savedSpeedBeforeLongPress\n                            isLongPressSpeedUp = false\n                            logger.fInfo { \"Long press speed restored: ${savedSpeedBeforeLongPress}x\" }\n                            return@onPreviewKeyEvent true\n                        }\n                        if (videoPlayer.isPlaying)\n                            onPause()\n                        else if (videoPlayer.currentPosition >= videoPlayer.duration) {\n                            goTime = 0\n                            onGoTime(0)\n                        } else\n                            onPlay()\n                        return@onPreviewKeyEvent false\n                    }\n\n                    // KEYCODE_CENTER_LONG\n                    // 一切设备上长按 DirectionCenter 键会是这个按键事件\n                    Key(763) -> {\n                        if (videoPlayerConfigData.longPressAction == 1) {\n                            if (it.type == KeyEventType.KeyDown && !isLongPressSpeedUp) {\n                                savedSpeedBeforeLongPress = videoPlayer.speed\n                                videoPlayer.speed = videoPlayerConfigData.longPressSpeed\n                                isLongPressSpeedUp = true\n                                logger.fInfo { \"KEYCODE_CENTER_LONG speed up: ${videoPlayerConfigData.longPressSpeed}x\" }\n                            } else if (it.type == KeyEventType.KeyUp && isLongPressSpeedUp) {\n                                videoPlayer.speed = savedSpeedBeforeLongPress\n                                isLongPressSpeedUp = false\n                                logger.fInfo { \"KEYCODE_CENTER_LONG speed restored: ${savedSpeedBeforeLongPress}x\" }\n                            }\n                        } else {\n                            scope.launch(Dispatchers.Main) {\n                                showMenuController = true\n                            }\n                        }\n                        return@onPreviewKeyEvent true\n                    }\n\n                    Key.DirectionUp -> {\n                        if (it.type == KeyEventType.KeyDown) return@onPreviewKeyEvent true\n                        if (videoPlayerConfigData.isLive) return@onPreviewKeyEvent true\n                        logger.info { \"[${it.key} press]\" }\n                        scope.launch(Dispatchers.Main) {\n                            showListController = true\n                        }\n                        return@onPreviewKeyEvent true\n                    }\n\n                    Key.DirectionDown -> {\n                        if (it.type == KeyEventType.KeyDown) {\n                            if (it.nativeKeyEvent.isLongPress) {\n                                logger.info { \"[${it.key} long press]\" }\n                                longPressDownTriggered = true\n                                doublePressDownJob?.cancel()\n                                lastPressDown = 0L\n                                onLoadNextVideo(false)\n                            }\n                            return@onPreviewKeyEvent true\n                        }\n                        // KeyUp 阶段\n                        // 如果之前触发过长按事件，则不执行后续逻辑\n                        if (longPressDownTriggered) {\n                            longPressDownTriggered = false\n                            return@onPreviewKeyEvent true\n                        }\n                        logger.info { \"[${it.key} press]\" }\n                        if (videoPlayerConfigData.isLive) {\n                            showInfo = true\n                            return@onPreviewKeyEvent true\n                        }\n\n                        // 检查是否为连按两次（间隔小于300ms且上次按键时间不为0）\n                        val currentTime = System.currentTimeMillis()\n                        val isDoublePress = lastPressDown != 0L && currentTime - lastPressDown < 300\n                        lastPressDown = currentTime\n\n                        doublePressDownJob?.cancel()\n                        doublePressDownJob = scope.launch(Dispatchers.Main) {\n                            delay(300)\n                            lastPressDown = 0L // 重置时间，避免第三次按下时误判\n                            if ((isDoublePress || showInfo) && !showRelatedVideos) {\n                                showInfo = false\n                                onToggleRelatedVideos(true)\n                            } else if(!showInfo && !showRelatedVideos) {\n                                showInfo = true\n                            }\n                        }\n                        return@onPreviewKeyEvent true\n                    }\n\n                    Key.Menu -> {\n                        if (it.type == KeyEventType.KeyDown) return@onPreviewKeyEvent true\n                        logger.info { \"[${it.key} press]\" }\n                        showMenuController = !showMenuController\n                        if(!showMenuController) onRequestFocus()\n                        return@onPreviewKeyEvent true\n                    }\n\n                    Key.Back -> {\n                        if (it.type == KeyEventType.KeyDown) return@onPreviewKeyEvent true\n                        logger.info { \"[${it.key} press]\" }\n\n                        // 有任何控制器显示中，先隐藏控制器\n                        if (showSeekController || showListController || showMenuController || showInfo || showRelatedVideos) {\n                            logger.fInfo { \"隐藏控制器\" }\n                            scope.launch(Dispatchers.Main) {\n                                showSeekController = false\n                                showListController = false\n                                showMenuController = false\n                                showInfo = false\n                                onToggleRelatedVideos(false)\n                                hideVideoInfoJob?.cancel()\n                            }\n                            return@onPreviewKeyEvent true\n                        }\n\n                        if (!videoPlayer.isPlaying) {\n                            logger.fInfo { \"Exiting video player\" }\n                            onExit()\n                            return@onPreviewKeyEvent true\n                        }\n\n                        val currentTime = System.currentTimeMillis()\n                        if (currentTime - lastPressBack < 1000 * 3) {\n                            logger.fInfo { \"Exiting video player\" }\n                            onExit()\n                        } else {\n                            lastPressBack = currentTime\n                            R.string.video_player_press_back_again_to_exit.toast(context)\n                        }\n                        return@onPreviewKeyEvent true\n                    }\n\n                    Key.MediaPlayPause -> {\n                        if (it.type == KeyEventType.KeyDown) return@onPreviewKeyEvent true\n                        logger.info { \"[${it.key} press]\" }\n                        if (videoPlayer.isPlaying) onPause() else onPlay()\n                        return@onPreviewKeyEvent true\n                    }\n\n                    Key.MediaPlay -> {\n                        if (it.type == KeyEventType.KeyDown) return@onPreviewKeyEvent true\n                        logger.info { \"[${it.key} press]\" }\n                        if (!videoPlayer.isPlaying) onPlay()\n                        return@onPreviewKeyEvent true\n                    }\n\n                    Key.MediaPause -> {\n                        if (it.type == KeyEventType.KeyDown) return@onPreviewKeyEvent true\n                        logger.info { \"[${it.key} press]\" }\n                        if (videoPlayer.isPlaying) onPause()\n                        return@onPreviewKeyEvent true\n                    }\n\n                    Key.MediaFastForward -> {\n                        if (it.type == KeyEventType.KeyUp) return@onPreviewKeyEvent true\n                        logger.info { \"[${it.key} press]\" }\n                        openSeekController()\n                        onTimeForward()\n                    }\n\n                    Key.MediaRewind -> {\n                        if (it.type == KeyEventType.KeyUp) return@onPreviewKeyEvent true\n                        logger.info { \"[${it.key} press]\" }\n                        openSeekController()\n                        onTimeBack()\n                    }\n\n                    Key.DirectionLeft -> {\n                        if (it.type == KeyEventType.KeyUp) return@onPreviewKeyEvent true\n                        logger.info { \"[${it.key} press]\" }\n                        openSeekController()\n                        onTimeBack()\n                    }\n\n                    Key.DirectionRight -> {\n                        if (it.type == KeyEventType.KeyUp) return@onPreviewKeyEvent true\n                        logger.info { \"[${it.key} press]\" }\n                        openSeekController()\n                        onTimeForward()\n                    }\n                }\n\n                false\n            }\n    ) {\n        content()\n        if (videoPlayerConfigData.showDebugInfo) {\n            val debugInfo by produceState(videoPlayerDebugInfoData.debugInfo) {\n                while (true) {\n                    value = videoPlayer.debugInfo\n                    delay(1000)\n                }\n            }\n            Box(\n                modifier = Modifier\n                    .align(Alignment.TopStart)\n                    .padding(8.dp)\n                    .clip(MaterialTheme.shapes.medium)\n                    .background(Color.Black.copy(alpha = 0.3f))\n            ) {\n                Text(\n                    modifier = Modifier.padding(8.dp),\n                    text = debugInfo,\n                    style = MaterialTheme.typography.bodySmall\n                )\n            }\n        }\n        BottomSubtitle()\n        SkipTips()\n        PlayStateTips(\n            canShowPause = !showInfo && !showSeekController\n        )\n        // 长按加速播放提示\n        if (isLongPressSpeedUp) {\n            Box(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .padding(bottom = 6.dp)\n                    .clip(MaterialTheme.shapes.extraLarge)\n                    .background(Color.Black.copy(alpha = 0.2f))\n            ) {\n                Text(\n                    modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),\n                    text = \"${videoPlayerConfigData.longPressSpeed}x 加速播放中\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = Color.White.copy(alpha = 0.55f)\n                )\n            }\n        }\n        ControllerVideoInfo(\n            show = showInfo,\n            playSpeed = videoPlayer.speed,\n            onHideInfo = { showInfo = false },\n            onPlay = {\n                if (videoPlayer.currentPosition >= videoPlayer.duration) {\n                    goTime = 0\n                    onGoTime(0)\n                } else {\n                    onPlay()\n                }\n            },\n            onPause = onPause,\n            onPlaySpeedChange = onPlaySpeedChange,\n            onOpenUpSpace = onOpenUpSpace,\n            onRefreshVideo = {\n                if (videoPlayer.duration > 0 && videoPlayer.currentPosition >= videoPlayer.duration) {\n                    goTime = 0\n                    onGoTime(0)\n                } else {\n                    onRefreshVideo()\n                }\n            },\n            onOpenDanmaku = onOpenDanmaku,\n            onHideDanmaku = onHideDanmaku,\n            onOpenPlayList = {\n                showInfo = false\n                showListController = true\n            },\n            onOpenRelatedVideo = {\n                if (!videoPlayerConfigData.isLive) {\n                    onToggleRelatedVideos(true)\n\n                    scope.launch(Dispatchers.Main) {\n                        delay(50)\n                        showInfo = false\n                    }\n                }\n            },\n            onOpenSetting = {\n                showInfo = false\n                showMenuController = true\n            },\n            onPlayModeChange = onPlayModeChange,\n            onRotationChange = onRotationChange,\n            userActionContent = userActionContent,\n            onSeekBack = {\n                scope.launch(Dispatchers.Main) {\n                    delay(100)\n                    openSeekController()\n                    onTimeBack()\n                }\n            },\n            onSeekForward = {\n                scope.launch(Dispatchers.Main) {\n                    delay(100)\n                    openSeekController()\n                    onTimeForward()\n                }\n            },\n            onSubtitleChange = onSubtitleChange,\n            onLoadNextVideo = onLoadNextVideo,\n            onShowComment = onShowComment,\n            onShowDescription = onShowDescription,\n            onResolutionChange = onResolutionChange,\n            onLiveQualityChange = onLiveQualityChange,\n            viewerCountText = viewerCountText\n        )\n        SeekController(\n            show = showSeekController,\n            goTime = goTime,\n            moveState = moveState\n        )\n        VideoListController(\n            show = showListController,\n            onPlayNewVideo = onPlayNewVideo\n        )\n        MenuController(\n            show = showMenuController,\n            onResolutionChange = onResolutionChange,\n            onCodecChange = onCodecChange,\n            onAspectRatioChange = onAspectRatioChange,\n            onRotationChange = onRotationChange,\n            onPlaySpeedChange = onPlaySpeedChange,\n            onAudioChange = onAudioChange,\n            onLiveQualityChange = onLiveQualityChange,\n            onLiveCodecChange = onLiveCodecChange,\n            onDanmakuSwitchChange = onDanmakuSwitchChange,\n            onDanmakuSizeChange = onDanmakuSizeChange,\n            onDanmakuOpacityChange = onDanmakuOpacityChange,\n            onDanmakuAreaChange = onDanmakuAreaChange,\n            onDanmakuMaskChange = onDanmakuMaskChange,\n            onDanmakuRollingDurationFactorChange = onDanmakuRollingDurationFactorChange,\n            onDanmakuFilterLevelChange = onDanmakuFilterLevelChange,\n            onSubtitleChange = onSubtitleChange,\n            onSubtitleSizeChange = onSubtitleSizeChange,\n            onSubtitleBackgroundOpacityChange = onSubtitleBackgroundOpacityChange,\n            onSubtitleBottomPadding = onSubtitleBottomPadding,\n            onPlayModeChange = onPlayModeChange,\n            onDebugInfoChange = onDebugInfoChange\n        )\n        // 缓存底部进度条显示条件，避免频繁计算\n        val shouldShowBottomProgressBar by remember { \n            derivedStateOf { \n                showBottomProgressBar && !showInfo && !showSeekController  && !videoPlayerConfigData.isLive\n            } \n        }\n        \n        // 底部常驻进度条组件\n        if (shouldShowBottomProgressBar) {\n            var throttledProgress by remember { mutableStateOf(0f) }\n            LaunchedEffect(shouldShowBottomProgressBar) {\n                while (isActive) {\n                    val duration = videoPlayerSeekState.duration\n                    throttledProgress = if (duration > 0) {\n                        videoPlayerSeekState.position.toFloat() / duration.toFloat()\n                    } else {\n                        0f\n                    }\n                    delay(1000)\n                }\n            }\n            \n            LinearProgressIndicator(\n                modifier = Modifier\n                    .align(Alignment.BottomCenter)\n                    .fillMaxWidth()\n                    .height(2.3.dp),\n                progress = { throttledProgress },\n                color = SliderDefaults.colors().activeTrackColor,\n                trackColor = Color.Black.copy(alpha = 0.4f),\n                gapSize = 0.dp,\n                drawStopIndicator = {}\n            )\n        }\n        // 推荐视频组件（在连按两次下键时显示）, UI实现在VideoPlayerV3Screen.kt中\n    }\n}\n"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/VideoShot.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalView\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.tooling.preview.PreviewParameter\nimport androidx.compose.ui.tooling.preview.PreviewParameterProvider\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.MaterialTheme\nimport dev.aaa1115910.biliapi.entity.video.VideoShot\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.tv.VideoSeekBar\nimport dev.aaa1115910.bv.player.util.getImage\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.delay\n\n@Composable\nfun VideoShot(\n    modifier: Modifier = Modifier,\n    videoShot: VideoShot,\n    position: Long,\n    duration: Long,\n    coercedOffset: Dp = 0.dp\n) {\n    val view = LocalView.current\n    val density = LocalDensity.current\n    val logger = KotlinLogging.logger {}\n\n    var bitmap by remember { mutableStateOf(ImageBitmap(1, 1)) }\n    var screenWidth by remember { mutableStateOf(0.dp) }\n    var coercedImageOffset by remember { mutableStateOf(0.dp) }\n    var imageWidth by remember { mutableStateOf(0.dp) }\n\n    LaunchedEffect(position, imageWidth) {\n        delay(50)\n        // logger.fInfo { \"update progress preview image offset at $position $imageWidth\" }\n        val baseOffset = -imageWidth / 2\n        val imageOffset = baseOffset + screenWidth * (position.toFloat() / duration.toFloat())\n        coercedImageOffset =\n            imageOffset.coerceIn(0.dp + coercedOffset, screenWidth - imageWidth - coercedOffset)\n    }\n\n    val positionSecond = remember(position) { position / 1000L }\n\n    LaunchedEffect(positionSecond) {\n        // logger.fInfo { \"update progress preview image at ${positionSecond * 1000L}\" }\n        if (!view.isInEditMode) {\n            bitmap = videoShot.getImage(positionSecond.toInt()).asImageBitmap()\n        }\n    }\n\n//    if (view.isInEditMode) {\n//        Text(\"offset: ${coercedImageOffset.value}\")\n//        Text(\"image width: ${imageWidth.value}\")\n//    }\n    BoxWithConstraints(\n        modifier = modifier.fillMaxWidth()\n    ) {\n        screenWidth = this.maxWidth\n        VideoShotImage(\n            modifier = Modifier\n                .offset(x = coercedImageOffset)\n                .onSizeChanged {\n                    imageWidth = with(density) { it.width.toDp() }\n                },\n            bitmap = bitmap\n        )\n    }\n}\n\n@Composable\nfun VideoShotImage(\n    modifier: Modifier = Modifier,\n    bitmap: ImageBitmap\n) {\n    val view = LocalView.current\n    val rotation = LocalVideoPlayerConfigData.current.currentVideoRotation\n\n    Image(\n        modifier = modifier\n            .height(100.dp)\n            .shadow(4.dp, MaterialTheme.shapes.medium)\n            .clip(MaterialTheme.shapes.medium)\n            .graphicsLayer {\n                rotationZ = rotation.degrees\n                if (rotation.shouldSwapDimensions && size.maxDimension > 0) {\n                    val s = size.minDimension / size.maxDimension\n                    scaleX = s\n                    scaleY = s\n                }\n            }\n            .drawBehind {\n                if (view.isInEditMode) {\n                    drawLine(Color.White, Offset(center.x, 0f), Offset(center.x, size.height), 2f)\n                }\n            },\n        bitmap = bitmap,\n        contentDescription = null,\n        contentScale = ContentScale.FillHeight\n    )\n}\n\n@Preview\n@Composable\nprivate fun VideoShotImagePreview() {\n    MaterialTheme {\n        VideoShotImage(bitmap = ImageBitmap(1, 1))\n    }\n}\n\n@Preview(device = \"id:tv_1080p\")\n@Composable\nprivate fun VideoShotPreview(@PreviewParameter(VideoShotProgressProvider::class) data: Pair<Long, Long>) {\n    MaterialTheme {\n        Column {\n            VideoShot(\n                videoShot = VideoShot(\n                    times = emptyList(),\n                    imageCountX = 0,\n                    imageCountY = 0,\n                    imageWidth = 0,\n                    imageHeight = 0,\n                    images = emptyList()\n                ),\n                position = data.second,\n                duration = data.first\n            )\n            VideoSeekBar(\n                duration = data.first,\n                position = data.second,\n                bufferedPercentage = 1\n            )\n        }\n    }\n}\n\nprivate class VideoShotProgressProvider : PreviewParameterProvider<Pair<Long, Long>> {\n    override val values = sequenceOf(\n        Pair(1234_000L, 0L),\n        Pair(1234_000L, 234_000L),\n        Pair(1234_000L, 555_000L),\n        Pair(1234_000L, 1234_000L)\n    )\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/playermenu/ClosedCaptionMenu.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller.playermenu\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport dev.aaa1115910.biliapi.entity.video.Subtitle\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerClosedCaptionMenuItem\nimport dev.aaa1115910.bv.player.tv.controller.LocalMenuFocusStateData\nimport dev.aaa1115910.bv.player.tv.controller.MenuFocusState\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.MenuListItem\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.RadioMenuList\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.StepLessMenuItem\nimport dev.aaa1115910.bv.util.ifElse\nimport java.text.NumberFormat\n\n@Composable\nfun ClosedCaptionMenuList(\n    modifier: Modifier = Modifier,\n    onSubtitleChange: (Subtitle) -> Unit,\n    onSubtitleSizeChange: (TextUnit) -> Unit,\n    onSubtitleBackgroundOpacityChange: (Float) -> Unit,\n    onSubtitleBottomPadding: (Dp) -> Unit,\n    onFocusStateChange: (MenuFocusState) -> Unit\n) {\n    val context = LocalContext.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val focusState = LocalMenuFocusStateData.current\n    val parentMenuFocusRequester = remember { FocusRequester() }\n    val parentMenuPositionFocusRequester = remember { FocusRequester() }\n    var selectedClosedCaptionMenuItem by remember { mutableStateOf(VideoPlayerClosedCaptionMenuItem.Switch) }\n\n    Row(\n        modifier = modifier.fillMaxHeight(),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        val menuItemsModifier = Modifier\n            .width(216.dp)\n            .padding(horizontal = 8.dp)\n        AnimatedVisibility(visible = focusState.focusState != MenuFocusState.MenuNav) {\n            when (selectedClosedCaptionMenuItem) {\n                VideoPlayerClosedCaptionMenuItem.Switch -> RadioMenuList(\n                    modifier = menuItemsModifier,\n                    items = videoPlayerConfigData.availableSubtitleTracks.map { it.langDoc },\n                    selected = videoPlayerConfigData.availableSubtitleTracks\n                        .indexOfFirst { it.id == videoPlayerConfigData.currentSubtitleId },\n                    onSelectedChanged = { onSubtitleChange(videoPlayerConfigData.availableSubtitleTracks[it]) },\n                    onFocusBackToParent = {\n                        onFocusStateChange(MenuFocusState.Menu)\n                        parentMenuFocusRequester.requestFocus()\n                    },\n                )\n\n                VideoPlayerClosedCaptionMenuItem.Size -> StepLessMenuItem(\n                    modifier = menuItemsModifier,\n                    value = videoPlayerConfigData.currentSubtitleFontSize.value.toInt(),\n                    step = 1,\n                    range = 12..48,\n                    text = \"${videoPlayerConfigData.currentSubtitleFontSize.value.toInt()} SP\",\n                    onValueChange = { onSubtitleSizeChange(it.sp) },\n                    onFocusBackToParent = { onFocusStateChange(MenuFocusState.Menu) }\n                )\n\n                VideoPlayerClosedCaptionMenuItem.Opacity -> StepLessMenuItem(\n                    modifier = menuItemsModifier,\n                    value = videoPlayerConfigData.currentSubtitleBackgroundOpacity,\n                    step = 0.01f,\n                    range = 0f..1f,\n                    text = NumberFormat.getPercentInstance()\n                        .apply { maximumFractionDigits = 0 }\n                        .format(videoPlayerConfigData.currentSubtitleBackgroundOpacity),\n                    onValueChange = onSubtitleBackgroundOpacityChange,\n                    onFocusBackToParent = { onFocusStateChange(MenuFocusState.Menu) }\n                )\n\n                VideoPlayerClosedCaptionMenuItem.Padding -> StepLessMenuItem(\n                    modifier = menuItemsModifier,\n                    value = videoPlayerConfigData.currentSubtitleBottomPadding.value.toInt(),\n                    step = 1,\n                    range = 0..48,\n                    text = \"${videoPlayerConfigData.currentSubtitleBottomPadding.value.toInt()} DP\",\n                    onValueChange = { onSubtitleBottomPadding(it.dp) },\n                    onFocusBackToParent = { onFocusStateChange(MenuFocusState.Menu) }\n                )\n            }\n        }\n\n        LazyColumn(\n            modifier = Modifier\n                .focusRequester(parentMenuFocusRequester)\n                .padding(horizontal = 8.dp)\n                .onPreviewKeyEvent {\n                    if (it.type == KeyEventType.KeyUp) {\n                        if (listOf(Key.Enter, Key.DirectionCenter).contains(it.key)) {\n                            return@onPreviewKeyEvent false\n                        }\n                        return@onPreviewKeyEvent true\n                    }\n                    when (it.key) {\n                        Key.DirectionRight -> onFocusStateChange(MenuFocusState.MenuNav)\n                        Key.DirectionLeft -> onFocusStateChange(MenuFocusState.Items)\n                        else -> {}\n                    }\n                    false\n                }\n                .focusRestorer(parentMenuPositionFocusRequester),\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n            contentPadding = PaddingValues(8.dp)\n        ) {\n            itemsIndexed(VideoPlayerClosedCaptionMenuItem.entries) { index, item ->\n                MenuListItem(\n                    modifier = Modifier\n                        .ifElse(\n                            index == 0,\n                            Modifier.focusRequester(parentMenuPositionFocusRequester)\n                        ),\n                    text = item.getDisplayName(context),\n                    selected = selectedClosedCaptionMenuItem == item,\n                    onClick = {},\n                    onFocus = { selectedClosedCaptionMenuItem = item },\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/playermenu/DanmakuMenu.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller.playermenu\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.entity.DanmakuType\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerDanmakuMenuItem\nimport dev.aaa1115910.bv.player.tv.controller.LocalMenuFocusStateData\nimport dev.aaa1115910.bv.player.tv.controller.MenuFocusState\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.CheckBoxMenuList\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.MenuListItem\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.RadioMenuList\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.StepLessMenuItem\nimport dev.aaa1115910.bv.util.ifElse\nimport java.text.NumberFormat\nimport kotlin.math.roundToInt\n\n@Composable\nfun DanmakuMenuList(\n    modifier: Modifier = Modifier,\n    onDanmakuSwitchChange: (List<DanmakuType>) -> Unit,\n    onDanmakuSizeChange: (Float) -> Unit,\n    onDanmakuOpacityChange: (Float) -> Unit,\n    onDanmakuAreaChange: (Float) -> Unit,\n    onDanmakuMaskChange: (Boolean) -> Unit,\n    onDanmakuRollingDurationFactorChange: (Float) -> Unit,\n    onDanmakuFilterLevelChange: (Int) -> Unit,\n    onFocusStateChange: (MenuFocusState) -> Unit\n) {\n    val context = LocalContext.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val focusState = LocalMenuFocusStateData.current\n    val parentMenuFocusRequester = remember { FocusRequester() }\n    val parentMenuPositionFocusRequester = remember { FocusRequester() }\n    var selectedDanmakuMenuItem by remember { mutableStateOf(VideoPlayerDanmakuMenuItem.Switch) }\n\n    Row(\n        modifier = modifier.fillMaxHeight(),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        val menuItemsModifier = Modifier\n            .width(216.dp)\n            .padding(horizontal = 8.dp)\n        AnimatedVisibility(visible = focusState.focusState != MenuFocusState.MenuNav) {\n            when (selectedDanmakuMenuItem) {\n                VideoPlayerDanmakuMenuItem.RollingDurationFactor -> StepLessMenuItem(\n                    modifier = menuItemsModifier,\n                    value = videoPlayerConfigData.currentDanmakuRollingDurationFactor,\n                    step = 0.1f,\n                    range = 0.2f..1.8f,\n                    text = \"${(videoPlayerConfigData.currentDanmakuRollingDurationFactor * 100).roundToInt() / 100f}x\",\n                    onValueChange = onDanmakuRollingDurationFactorChange,\n                    onFocusBackToParent = { onFocusStateChange(MenuFocusState.Menu) }\n                )\n\n                VideoPlayerDanmakuMenuItem.Switch -> CheckBoxMenuList(\n                    modifier = menuItemsModifier,\n                    items = DanmakuType.entries.map { it.getDisplayName(context) },\n                    selected = videoPlayerConfigData.currentDanmakuEnabledList.map { it.ordinal },\n                    onSelectedChanged = {\n                        val newEnabledDanmakuList = it\n                            .map { index -> DanmakuType.entries[index] }\n                            .toMutableList()\n\n                        if (\n                            newEnabledDanmakuList.contains(DanmakuType.All) &&\n                            !videoPlayerConfigData.currentDanmakuEnabledList.contains(DanmakuType.All)\n                        ) {\n                            // 勾选了全部\n                            onDanmakuSwitchChange(DanmakuType.entries)\n                        } else if (\n                            videoPlayerConfigData.currentDanmakuEnabledList.contains(DanmakuType.All) &&\n                            !newEnabledDanmakuList.contains(DanmakuType.All)\n                        ) {\n                            // 取消了全部\n                            onDanmakuSwitchChange(listOf())\n                        } else if (\n                            videoPlayerConfigData.currentDanmakuEnabledList.contains(DanmakuType.All) &&\n                            newEnabledDanmakuList.contains(DanmakuType.All) &&\n                            videoPlayerConfigData.currentDanmakuEnabledList.size != newEnabledDanmakuList.size\n                        ) {\n                            // 在勾选全部时，取消某一项\n                            newEnabledDanmakuList.remove(DanmakuType.All)\n                            onDanmakuSwitchChange(newEnabledDanmakuList)\n                        } else if (\n                            !videoPlayerConfigData.currentDanmakuEnabledList.contains(DanmakuType.All) &&\n                            newEnabledDanmakuList.size == DanmakuType.entries.size - 1\n                        ) {\n                            // 在勾选了全部之外的所有项时，勾选全部项\n                            onDanmakuSwitchChange(DanmakuType.entries)\n                        } else {\n                            onDanmakuSwitchChange(newEnabledDanmakuList)\n                        }\n                    },\n                    onFocusBackToParent = {\n                        onFocusStateChange(MenuFocusState.Menu)\n                        parentMenuFocusRequester.requestFocus()\n                    }\n                )\n\n                VideoPlayerDanmakuMenuItem.Size -> StepLessMenuItem(\n                    modifier = menuItemsModifier,\n                    value = videoPlayerConfigData.currentDanmakuScale,\n                    step = 0.01f,\n                    range = 0.5f..4f,\n                    text = NumberFormat.getPercentInstance()\n                        .apply { maximumFractionDigits = 0 }\n                        .format(videoPlayerConfigData.currentDanmakuScale),\n                    onValueChange = onDanmakuSizeChange,\n                    onFocusBackToParent = { onFocusStateChange(MenuFocusState.Menu) }\n                )\n\n                VideoPlayerDanmakuMenuItem.Opacity -> StepLessMenuItem(\n                    modifier = menuItemsModifier,\n                    value = videoPlayerConfigData.currentDanmakuOpacity,\n                    step = 0.01f,\n                    range = 0f..1f,\n                    text = NumberFormat.getPercentInstance()\n                        .apply { maximumFractionDigits = 0 }\n                        .format(videoPlayerConfigData.currentDanmakuOpacity),\n                    onValueChange = onDanmakuOpacityChange,\n                    onFocusBackToParent = { onFocusStateChange(MenuFocusState.Menu) }\n                )\n\n                VideoPlayerDanmakuMenuItem.Area -> StepLessMenuItem(\n                    modifier = menuItemsModifier,\n                    value = videoPlayerConfigData.currentDanmakuArea,\n                    step = 0.01f,\n                    range = 0f..1f,\n                    text = NumberFormat.getPercentInstance()\n                        .apply { maximumFractionDigits = 0 }\n                        .format(videoPlayerConfigData.currentDanmakuArea),\n                    onValueChange = onDanmakuAreaChange,\n                    onFocusBackToParent = { onFocusStateChange(MenuFocusState.Menu) }\n                )\n\n                VideoPlayerDanmakuMenuItem.Mask -> RadioMenuList(\n                    modifier = menuItemsModifier,\n                    items = listOf(\"关闭\", \"开启\"),\n                    selected = if (videoPlayerConfigData.currentDanmakuMask) 1 else 0,\n                    onSelectedChanged = { onDanmakuMaskChange(it == 1) },\n                    onFocusBackToParent = {\n                        onFocusStateChange(MenuFocusState.Menu)\n                        parentMenuFocusRequester.requestFocus()\n                    }\n                )\n\n                VideoPlayerDanmakuMenuItem.FilterLevel -> {\n                    val (minValue, maxValue) = if (videoPlayerConfigData.isLive) 0 to 60 else 0 to 10\n                    val currentValue = if (videoPlayerConfigData.isLive)\n                        videoPlayerConfigData.currentLiveDanmakuFilterLevel\n                    else\n                        videoPlayerConfigData.currentDanmakuFilterLevel\n\n                    StepLessMenuItem(\n                        modifier = menuItemsModifier,\n                        value = currentValue.toFloat(),\n                        step = 1f,\n                        range = minValue.toFloat()..maxValue.toFloat(),\n                        text = \"过滤<${currentValue}的\",\n                        onValueChange = { onDanmakuFilterLevelChange(it.toInt()) },\n                        onFocusBackToParent = { onFocusStateChange(MenuFocusState.Menu) }\n                    )\n                }\n            }\n        }\n\n        LazyColumn(\n            modifier = Modifier\n                .focusRequester(parentMenuFocusRequester)\n                .padding(horizontal = 8.dp)\n                .onPreviewKeyEvent {\n                    if (it.type == KeyEventType.KeyUp) {\n                        if (listOf(Key.Enter, Key.DirectionCenter).contains(it.key)) {\n                            return@onPreviewKeyEvent false\n                        }\n                        return@onPreviewKeyEvent true\n                    }\n                    when (it.key) {\n                        Key.DirectionRight -> onFocusStateChange(MenuFocusState.MenuNav)\n                        Key.DirectionLeft -> onFocusStateChange(MenuFocusState.Items)\n                        else -> {}\n                    }\n                    false\n                }\n                .focusRestorer(parentMenuPositionFocusRequester),\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n            contentPadding = PaddingValues(8.dp)\n        ) {\n            itemsIndexed(VideoPlayerDanmakuMenuItem.entries) { index, item ->\n                MenuListItem(\n                    modifier = Modifier\n                        .ifElse(\n                            index == 0,\n                            Modifier.focusRequester(parentMenuPositionFocusRequester)\n                        ),\n                    text = item.getDisplayName(context, isLive = videoPlayerConfigData.isLive),\n                    selected = selectedDanmakuMenuItem == item,\n                    onClick = {},\n                    onFocus = { selectedDanmakuMenuItem = item },\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/playermenu/MenuNav.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller.playermenu\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.VideoPlayerMenuNavItem\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.MenuListItem\nimport dev.aaa1115910.bv.util.ifElse\n\n@Composable\nfun MenuNavList(\n    modifier: Modifier = Modifier,\n    selectedMenu: VideoPlayerMenuNavItem,\n    onSelectedChanged: (VideoPlayerMenuNavItem) -> Unit,\n    isFocusing: Boolean\n) {\n    val context = LocalContext.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val focusRequester = remember { FocusRequester() }\n    val navItems = remember(videoPlayerConfigData.isLive) {\n        VideoPlayerMenuNavItem.entries.toMutableList().apply {\n            if (videoPlayerConfigData.isLive) {\n                remove(VideoPlayerMenuNavItem.ClosedCaption)\n                remove(VideoPlayerMenuNavItem.Others)\n            }\n        }\n    }\n\n    LazyColumn(\n        modifier = modifier\n            .focusRestorer(focusRequester),\n        verticalArrangement = Arrangement.spacedBy(8.dp),\n        contentPadding = PaddingValues(16.dp)\n    ) {\n        itemsIndexed(navItems) { index, item ->\n            MenuListItem(\n                modifier = Modifier\n                    .ifElse(index == 0, Modifier.focusRequester(focusRequester)),\n                text = item.getDisplayName(context),\n                icon = item.icon,\n                expanded = isFocusing,\n                selected = selectedMenu == item,\n                onClick = {},\n                onFocus = { onSelectedChanged(item) },\n            )\n        }\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/playermenu/OthersMenu.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller.playermenu\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.PlayMode\nimport dev.aaa1115910.bv.player.entity.VideoPlayerOthersMenuItem\nimport dev.aaa1115910.bv.player.tv.controller.LocalMenuFocusStateData\nimport dev.aaa1115910.bv.player.tv.controller.MenuFocusState\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.MenuListItem\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.RadioMenuList\nimport dev.aaa1115910.bv.util.ifElse\n\n@Composable\nfun OthersMenuList(\n    modifier: Modifier = Modifier,\n    onPlayModeChange: (PlayMode) -> Unit,\n    onDebugInfoChange: (Boolean) -> Unit = {},\n    onFocusStateChange: (MenuFocusState) -> Unit\n) {\n    val context = LocalContext.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val focusState = LocalMenuFocusStateData.current\n    val parentMenuFocusRequester = remember { FocusRequester() }\n    val parentMenuPositionFocusRequester = remember { FocusRequester() }\n    var selectedOthersMenuItem by remember { mutableStateOf(VideoPlayerOthersMenuItem.PlayMode) }\n\n    Row(\n        modifier = modifier.fillMaxHeight(),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        val menuItemsModifier = Modifier\n            .width(216.dp)\n            .padding(horizontal = 8.dp)\n        AnimatedVisibility(visible = focusState.focusState != MenuFocusState.MenuNav) {\n            when (selectedOthersMenuItem) {\n                VideoPlayerOthersMenuItem.PlayMode -> {\n                    val availableModes = PlayMode.entries.filter { mode ->\n                        when (mode) {\n                            PlayMode.ListOrder -> videoPlayerConfigData.hasPreloadedVideoList && !videoPlayerConfigData.fromSeason\n                            PlayMode.ListOrderReverse -> videoPlayerConfigData.hasPreloadedVideoList && !videoPlayerConfigData.fromSeason\n                            PlayMode.RelatedVideo -> videoPlayerConfigData.hasRelatedVideos && !videoPlayerConfigData.fromSeason\n                            else -> true\n                        }\n                    }\n                    val effectivePlayMode = if (videoPlayerConfigData.currentPlayMode in availableModes)\n                        videoPlayerConfigData.currentPlayMode else PlayMode.PartAndEpisode\n                    RadioMenuList(\n                        modifier = menuItemsModifier,\n                        items = availableModes.map { it.getDisplayName(context) },\n                        selected = availableModes.indexOf(effectivePlayMode),\n                        onSelectedChanged = { onPlayModeChange(availableModes[it]) },\n                        onFocusBackToParent = {\n                            onFocusStateChange(MenuFocusState.Menu)\n                            parentMenuFocusRequester.requestFocus()\n                        }\n                    )\n                }\n\n                VideoPlayerOthersMenuItem.DebugInfo -> {\n                    RadioMenuList(\n                        modifier = menuItemsModifier,\n                        items = listOf(\"关闭\", \"开启\"),\n                        selected = if (videoPlayerConfigData.showDebugInfo) 1 else 0,\n                        onSelectedChanged = { onDebugInfoChange(it == 1) },\n                        onFocusBackToParent = {\n                            onFocusStateChange(MenuFocusState.Menu)\n                            parentMenuFocusRequester.requestFocus()\n                        }\n                    )\n                }\n            }\n        }\n\n        LazyColumn(\n            modifier = Modifier\n                .focusRequester(parentMenuFocusRequester)\n                .padding(horizontal = 8.dp)\n                .onPreviewKeyEvent {\n                    if (it.type == KeyEventType.KeyUp) {\n                        if (listOf(Key.Enter, Key.DirectionCenter).contains(it.key)) {\n                            return@onPreviewKeyEvent false\n                        }\n                        return@onPreviewKeyEvent true\n                    }\n                    when (it.key) {\n                        Key.DirectionRight -> onFocusStateChange(MenuFocusState.MenuNav)\n                        Key.DirectionLeft -> onFocusStateChange(MenuFocusState.Items)\n                        else -> {}\n                    }\n                    false\n                }\n                .focusRestorer(parentMenuPositionFocusRequester),\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n            contentPadding = PaddingValues(8.dp)\n        ) {\n            itemsIndexed(VideoPlayerOthersMenuItem.entries.toMutableList().apply {\n                if (videoPlayerConfigData.isLive) {\n                    remove(VideoPlayerOthersMenuItem.PlayMode)\n                }\n            }) { index, item ->\n                MenuListItem(\n                    modifier = Modifier\n                        .ifElse(\n                            index == 0,\n                            Modifier.focusRequester(parentMenuPositionFocusRequester)\n                        ),\n                    text = item.getDisplayName(context),\n                    selected = selectedOthersMenuItem == item,\n                    onClick = {},\n                    onFocus = { selectedOthersMenuItem = item },\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/playermenu/PictureMenu.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller.playermenu\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.player.entity.Audio\nimport dev.aaa1115910.bv.player.entity.LiveCodec\nimport dev.aaa1115910.bv.player.entity.LocalVideoPlayerConfigData\nimport dev.aaa1115910.bv.player.entity.Resolution\nimport dev.aaa1115910.bv.player.entity.VideoAspectRatio\nimport dev.aaa1115910.bv.player.entity.VideoCodec\nimport dev.aaa1115910.bv.player.entity.VideoPlayerPictureMenuItem\nimport dev.aaa1115910.bv.player.entity.VideoRotation\nimport dev.aaa1115910.bv.player.tv.controller.LocalMenuFocusStateData\nimport dev.aaa1115910.bv.player.tv.controller.MenuFocusState\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.MenuListItem\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.RadioMenuList\nimport dev.aaa1115910.bv.player.tv.controller.playermenu.component.StepLessMenuItem\nimport dev.aaa1115910.bv.util.ifElse\nimport kotlin.math.roundToInt\n\n@Composable\nfun PictureMenuList(\n    modifier: Modifier = Modifier,\n    onResolutionChange: (Resolution) -> Unit,\n    onCodecChange: (VideoCodec) -> Unit,\n    onAspectRatioChange: (VideoAspectRatio) -> Unit,\n    onRotationChange: (VideoRotation) -> Unit,\n    onPlaySpeedChange: (Float) -> Unit,\n    onAudioChange: (Audio) -> Unit,\n    onLiveQualityChange: (Int) -> Unit = {},\n    onLiveCodecChange: (LiveCodec) -> Unit = {},\n    onFocusStateChange: (MenuFocusState) -> Unit\n) {\n    val context = LocalContext.current\n    val focusState = LocalMenuFocusStateData.current\n    val videoPlayerConfigData = LocalVideoPlayerConfigData.current\n    val parentMenuFocusRequester = remember { FocusRequester() }\n    val parentMenuPositionFocusRequester = remember { FocusRequester() }\n    var selectedPictureMenuItem by remember { mutableStateOf(VideoPlayerPictureMenuItem.PlaySpeed) }\n    val resolutionList = remember(videoPlayerConfigData.availableResolutions) {\n        videoPlayerConfigData.availableResolutions.sortedByDescending { it.code }\n    }\n    val audioList = remember(videoPlayerConfigData.availableAudio) {\n        videoPlayerConfigData.availableAudio.sortedBy { it.ordinal }\n    }\n\n    Row(\n        modifier = modifier.fillMaxHeight(),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        val menuItemsModifier = Modifier\n            .width(216.dp)\n            .padding(horizontal = 8.dp)\n        AnimatedVisibility(visible = focusState.focusState != MenuFocusState.MenuNav) {\n            when (selectedPictureMenuItem) {\n                VideoPlayerPictureMenuItem.Resolution -> if (videoPlayerConfigData.isLive && videoPlayerConfigData.availableLiveQualities.isNotEmpty()) {\n                    val liveQualities = videoPlayerConfigData.availableLiveQualities\n                    val selectedIndex = liveQualities.indexOfFirst { it.first == videoPlayerConfigData.currentLiveQn }.coerceAtLeast(0)\n                    RadioMenuList(\n                        modifier = menuItemsModifier,\n                        items = liveQualities.map { it.second },\n                        selected = selectedIndex,\n                        onSelectedChanged = { onLiveQualityChange(liveQualities[it].first) },\n                        onFocusBackToParent = {\n                            onFocusStateChange(MenuFocusState.Menu)\n                            parentMenuFocusRequester.requestFocus()\n                        }\n                    )\n                } else {\n                    RadioMenuList(\n                        modifier = menuItemsModifier,\n                        items = resolutionList.map { resolution ->\n                            resolution.getShortDisplayName(context)\n                        },\n                        selected = resolutionList.indexOf(videoPlayerConfigData.currentResolution),\n                        onSelectedChanged = { onResolutionChange(resolutionList[it]) },\n                        onFocusBackToParent = {\n                            onFocusStateChange(MenuFocusState.Menu)\n                            parentMenuFocusRequester.requestFocus()\n                        }\n                    )\n                }\n\n                VideoPlayerPictureMenuItem.Codec -> if (videoPlayerConfigData.isLive) {\n                    // 直播模式：显示 HLS/FLV/AVC 选项\n                    println(\"PictureMenu: isLive=true, availableLiveCodecs=${videoPlayerConfigData.availableLiveCodecs}, currentLiveCodec=${videoPlayerConfigData.currentLiveCodec}\")\n                    RadioMenuList(\n                        modifier = menuItemsModifier,\n                        items = videoPlayerConfigData.availableLiveCodecs\n                            .map { it.getDisplayName(context) },\n                        selected = videoPlayerConfigData.availableLiveCodecs\n                            .indexOf(videoPlayerConfigData.currentLiveCodec),\n                        onSelectedChanged = {\n                            println(\"PictureMenu: onSelectedChanged called with index=$it\")\n                            onLiveCodecChange(videoPlayerConfigData.availableLiveCodecs[it])\n                        },\n                        onFocusBackToParent = {\n                            onFocusStateChange(MenuFocusState.Menu)\n                            parentMenuFocusRequester.requestFocus()\n                        }\n                    )\n                } else {\n                    // 点播模式：显示原有编码选项\n                    RadioMenuList(\n                        modifier = menuItemsModifier,\n                        items = videoPlayerConfigData.availableVideoCodec\n                            .map { it.getDisplayName(context) },\n                        selected = videoPlayerConfigData.availableVideoCodec\n                            .indexOf(videoPlayerConfigData.currentVideoCodec),\n                        onSelectedChanged = { onCodecChange(videoPlayerConfigData.availableVideoCodec[it]) },\n                        onFocusBackToParent = {\n                            onFocusStateChange(MenuFocusState.Menu)\n                            parentMenuFocusRequester.requestFocus()\n                        }\n                    )\n                }\n\n                VideoPlayerPictureMenuItem.AspectRatio -> RadioMenuList(\n                    modifier = menuItemsModifier,\n                    items = VideoAspectRatio.entries.map { it.getDisplayName(context) },\n                    selected = VideoAspectRatio.entries\n                        .indexOf(videoPlayerConfigData.currentVideoAspectRatio),\n                    onSelectedChanged = { onAspectRatioChange(VideoAspectRatio.entries[it]) },\n                    onFocusBackToParent = {\n                        onFocusStateChange(MenuFocusState.Menu)\n                        parentMenuFocusRequester.requestFocus()\n                    }\n                )\n\n                VideoPlayerPictureMenuItem.Rotation -> RadioMenuList(\n                    modifier = menuItemsModifier,\n                    items = VideoRotation.entries.map { it.getDisplayName(context) },\n                    selected = VideoRotation.entries\n                        .indexOf(videoPlayerConfigData.currentVideoRotation),\n                    onSelectedChanged = { onRotationChange(VideoRotation.entries[it]) },\n                    onFocusBackToParent = {\n                        onFocusStateChange(MenuFocusState.Menu)\n                        parentMenuFocusRequester.requestFocus()\n                    }\n                )\n\n                VideoPlayerPictureMenuItem.PlaySpeed -> StepLessMenuItem(\n                    modifier = menuItemsModifier,\n                    value = videoPlayerConfigData.currentVideoSpeed,\n                    step = 0.25f,\n                    range = 0.25f..3f,\n                    text = \"${(videoPlayerConfigData.currentVideoSpeed * 100).roundToInt() / 100f}x\",\n                    onValueChange = onPlaySpeedChange,\n                    onFocusBackToParent = { onFocusStateChange(MenuFocusState.Menu) }\n                )\n\n                VideoPlayerPictureMenuItem.Audio -> RadioMenuList(\n                    modifier = menuItemsModifier,\n                    items = audioList.map { audio -> audio.getDisplayName(context) },\n                    selected = audioList.indexOf(videoPlayerConfigData.currentAudio),\n                    onSelectedChanged = { onAudioChange(audioList[it]) },\n                    onFocusBackToParent = {\n                        onFocusStateChange(MenuFocusState.Menu)\n                        parentMenuFocusRequester.requestFocus()\n                    }\n                )\n            }\n        }\n\n        LazyColumn(\n            modifier = Modifier\n                .focusRequester(parentMenuFocusRequester)\n                .padding(horizontal = 8.dp)\n                .onPreviewKeyEvent {\n                    if (it.type == KeyEventType.KeyUp) {\n                        if (listOf(Key.Enter, Key.DirectionCenter).contains(it.key)) {\n                            return@onPreviewKeyEvent false\n                        }\n                        return@onPreviewKeyEvent true\n                    }\n                    when (it.key) {\n                        Key.DirectionRight -> onFocusStateChange(MenuFocusState.MenuNav)\n                        Key.DirectionLeft -> onFocusStateChange(MenuFocusState.Items)\n                        else -> {}\n                    }\n                    false\n                }\n                .focusRestorer(parentMenuPositionFocusRequester),\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n            contentPadding = PaddingValues(8.dp)\n        ) {\n            itemsIndexed(VideoPlayerPictureMenuItem.entries.toMutableList().apply {\n                if (videoPlayerConfigData.isLive) {\n                    remove(VideoPlayerPictureMenuItem.PlaySpeed)\n                    remove(VideoPlayerPictureMenuItem.Codec)\n                    remove(VideoPlayerPictureMenuItem.Audio)\n                }\n            }) { index, item ->\n                MenuListItem(\n                    modifier = Modifier\n                        .ifElse(\n                            index == 0,\n                            Modifier.focusRequester(parentMenuPositionFocusRequester)\n                        ),\n                    text = item.getDisplayName(context),\n                    selected = selectedPictureMenuItem == item,\n                    onClick = {},\n                    onFocus = { selectedPictureMenuItem = item },\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/playermenu/component/CheckBoxMenuList.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller.playermenu.component\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.util.ifElse\n\n@Composable\nfun CheckBoxMenuList(\n    modifier: Modifier = Modifier,\n    items: List<String>,\n    selected: List<Int> = listOf(),\n    onSelectedChanged: (indexes: List<Int>) -> Unit,\n    onFocusBackToParent: () -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n    LazyColumn(\n        modifier = modifier\n            .onPreviewKeyEvent {\n                println(it)\n                if (it.type == KeyEventType.KeyUp) {\n                    if (listOf(Key.Enter, Key.DirectionCenter).contains(it.key)) {\n                        return@onPreviewKeyEvent false\n                    }\n                    return@onPreviewKeyEvent true\n                }\n                val result = it.key == Key.DirectionRight\n                if (result) onFocusBackToParent()\n                result\n            }\n            .focusRestorer(focusRequester),\n        verticalArrangement = Arrangement.spacedBy(8.dp),\n        contentPadding = PaddingValues(vertical = 120.dp, horizontal = 8.dp)\n    ) {\n        itemsIndexed(items) { index, item ->\n            MenuListItem(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .ifElse(index == 0, Modifier.focusRequester(focusRequester)),\n                text = item,\n                selected = selected.contains(index),\n                onClick = {\n                    val newSelectedIndexes = selected.toMutableList()\n                    if (newSelectedIndexes.contains(index)) newSelectedIndexes.remove(index)\n                    else newSelectedIndexes.add(index)\n                    onSelectedChanged(newSelectedIndexes)\n                }\n            )\n        }\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/playermenu/component/MenuListItem.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller.playermenu.component\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Home\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.DenseListItem\nimport androidx.tv.material3.Icon\nimport androidx.tv.material3.ListItemDefaults\nimport androidx.tv.material3.MaterialTheme\nimport androidx.tv.material3.Text\n\n@Composable\nfun MenuListItem(\n    modifier: Modifier = Modifier,\n    text: String,\n    icon: ImageVector? = null,\n    expanded: Boolean = true,\n    selected: Boolean,\n    textAlign: TextAlign = TextAlign.Center,\n    onFocus: () -> Unit = {},\n    onClick: () -> Unit\n) {\n    val itemWidth by animateDpAsState(\n        targetValue = if (expanded) 200.dp else 66.dp,\n        animationSpec = spring(\n            dampingRatio = Spring.DampingRatioNoBouncy,\n            stiffness = Spring.StiffnessLow\n        ),\n        label = \"MenuListItem width [$text]\"\n    )\n\n    DenseListItem(\n        modifier = modifier\n            .width(itemWidth)\n            .onFocusChanged { if (it.hasFocus) onFocus() },\n        selected = selected,\n        onClick = onClick,\n        headlineContent = {\n            Box {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    AnimatedVisibility(\n                        visible = expanded,\n                        label = \"MenuListItem text [$text]\",\n                        enter = fadeIn(),\n                        exit = fadeOut()\n                    ) {\n                        Text(\n                            modifier = Modifier.fillMaxWidth(),\n                            text = text,\n                            style = MaterialTheme.typography.titleLarge,\n                            textAlign = textAlign,\n                            maxLines = 1\n                        )\n                    }\n                }\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth(),\n                    horizontalArrangement = Arrangement.Center\n                ) {\n                    AnimatedVisibility(\n                        visible = !expanded,\n                        label = \"MenuListItem icon [$text]\",\n                        enter = fadeIn(),\n                        exit = fadeOut()\n                    ) {\n                        if (icon == null) {\n                            Box(modifier = Modifier.size(32.dp))\n                        } else {\n                            Icon(\n                                modifier = Modifier.size(32.dp),\n                                imageVector = icon,\n                                contentDescription = null\n                            )\n                        }\n                    }\n                }\n            }\n        },\n        colors = ListItemDefaults.colors(\n            selectedContainerColor = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.4f),\n        )\n    )\n}\n\n@Preview\n@Composable\nfun MenuListItemPreview() {\n    var expanded by remember { mutableStateOf(true) }\n    MaterialTheme {\n        MenuListItem(\n            text = \"MenuListItem\",\n            icon = Icons.Default.Home,\n            expanded = expanded,\n            selected = true,\n            textAlign = TextAlign.Center,\n            onFocus = {},\n            onClick = { expanded = !expanded }\n        )\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/playermenu/component/RadioMenuList.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller.playermenu.component\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.focusRestorer\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.unit.dp\nimport dev.aaa1115910.bv.util.ifElse\n\n@Composable\nfun RadioMenuList(\n    modifier: Modifier = Modifier,\n    items: List<String>,\n    selected: Int = 0,\n    onSelectedChanged: (index: Int) -> Unit,\n    onFocusBackToParent: () -> Unit\n) {\n    val focusRequester = remember { FocusRequester() }\n    LazyColumn(\n        modifier = modifier\n            .onPreviewKeyEvent {\n                println(it)\n                if (it.type == KeyEventType.KeyUp) {\n                    if (listOf(Key.Enter, Key.DirectionCenter).contains(it.key)) {\n                        return@onPreviewKeyEvent false\n                    }\n                    return@onPreviewKeyEvent true\n                }\n                val result = it.key == Key.DirectionRight\n                if (result) onFocusBackToParent()\n                result\n            }\n            .focusRestorer(focusRequester),\n        verticalArrangement = Arrangement.spacedBy(8.dp),\n        contentPadding = PaddingValues(vertical = 120.dp, horizontal = 8.dp)\n    ) {\n        itemsIndexed(items) { index, item ->\n            MenuListItem(\n                modifier = Modifier\n                    .width(200.dp)\n                    .ifElse(selected == index, Modifier.focusRequester(focusRequester)),\n                text = item,\n                selected = selected == index,\n                onClick = {\n                    println(\"Click menu: $item ($index)\")\n                    onSelectedChanged(index)\n                }\n            )\n        }\n    }\n}"
  },
  {
    "path": "player/tv/src/main/kotlin/dev/aaa1115910/bv/player/tv/controller/playermenu/component/StepLessMenuItem.kt",
    "content": "package dev.aaa1115910.bv.player.tv.controller.playermenu.component\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.ArrowDropDown\nimport androidx.compose.material.icons.rounded.ArrowDropUp\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onPreviewKeyEvent\nimport androidx.compose.ui.input.key.type\nimport androidx.compose.ui.unit.dp\nimport androidx.tv.material3.Icon\n\n@Composable\nfun StepLessMenuItem(\n    modifier: Modifier = Modifier,\n    value: Float = 1f,\n    text: String,\n    step: Float = 0.01f,\n    range: ClosedFloatingPointRange<Float> = 0f..1f,\n    onValueChange: (Float) -> Unit,\n    onFocusBackToParent: () -> Unit\n) {\n    Box(\n        modifier = modifier\n            .fillMaxHeight()\n            .onPreviewKeyEvent {\n                println(it)\n                if (it.type == KeyEventType.KeyUp) return@onPreviewKeyEvent true\n                if (it.key == Key.DirectionRight) onFocusBackToParent()\n                false\n            }\n    ) {\n        Column(\n            modifier = Modifier\n                .align(Alignment.Center)\n                .padding(horizontal = 8.dp),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Icon(imageVector = Icons.Rounded.ArrowDropUp, contentDescription = null)\n            MenuListItem(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .onPreviewKeyEvent {\n                        when (it.key) {\n                            Key.DirectionUp -> {\n                                if (it.type == KeyEventType.KeyUp) return@onPreviewKeyEvent true\n                                if (value >= range.endInclusive - step) {\n                                    onValueChange(range.endInclusive)\n                                } else {\n                                    onValueChange(value + step)\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n\n                            Key.DirectionDown -> {\n                                if (it.type == KeyEventType.KeyUp) return@onPreviewKeyEvent true\n                                if (value - step <= range.start) {\n                                    onValueChange(range.start)\n                                } else {\n                                    onValueChange(value - step)\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                        }\n                        false\n                    },\n                text = text,\n                selected = false\n            ) { }\n            Icon(imageVector = Icons.Rounded.ArrowDropDown, contentDescription = null)\n        }\n    }\n}\n\n@Composable\nfun StepLessMenuItem(\n    modifier: Modifier = Modifier,\n    value: Int = 100,\n    text: String,\n    step: Int = 1,\n    range: IntRange = 0..100,\n    onValueChange: (Int) -> Unit,\n    onFocusBackToParent: () -> Unit\n) {\n    Box(\n        modifier = modifier\n            .fillMaxHeight()\n            .onPreviewKeyEvent {\n                println(it)\n                if (it.type == KeyEventType.KeyUp) return@onPreviewKeyEvent true\n                if (it.key == Key.DirectionRight) onFocusBackToParent()\n                false\n            }\n    ) {\n        Column(\n            modifier = Modifier\n                .align(Alignment.Center)\n                .padding(horizontal = 8.dp),\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Icon(imageVector = Icons.Rounded.ArrowDropUp, contentDescription = null)\n            MenuListItem(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .onPreviewKeyEvent {\n                        when (it.key) {\n                            Key.DirectionUp -> {\n                                if (it.type == KeyEventType.KeyUp) return@onPreviewKeyEvent true\n                                if (value >= range.last - step) {\n                                    onValueChange(range.last)\n                                } else {\n                                    onValueChange(value + step)\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n\n                            Key.DirectionDown -> {\n                                if (it.type == KeyEventType.KeyUp) return@onPreviewKeyEvent true\n                                if (value - step <= range.first) {\n                                    onValueChange(range.first)\n                                } else {\n                                    onValueChange(value - step)\n                                }\n                                return@onPreviewKeyEvent true\n                            }\n                        }\n                        false\n                    },\n                text = text,\n                selected = false\n            ) { }\n            Icon(imageVector = Icons.Rounded.ArrowDropDown, contentDescription = null)\n        }\n    }\n}\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        maven(\"https://jitpack.io\")\n        maven(\"https://repo1.maven.org/maven2/\")\n        maven(\"https://androidx.dev/storage/compose-compiler/repository/\")\n        //maven(\"https://artifact.bytedance.com/repository/releases/\")\n    }\n    versionCatalogs {\n        create(\"androidx\") { from(files(\"gradle/androidx.versions.toml\")) }\n        create(\"gradleLibs\") { from(files(\"gradle/gradle.versions.toml\")) }\n    }\n}\nrootProject.name = \"BV\"\ninclude(\":app\")\ninclude(\":app:mobile\")\ninclude(\":app:shared\")\ninclude(\":app:tv\")\ninclude(\":bili-api\")\ninclude(\":bili-api:grpc\")\ninclude(\":bili-subtitle\")\ninclude(\":libs:av1Decoder\")\ninclude(\":libs:ffmpegDecoder\")\ninclude(\":libs:libVLC\")\ninclude(\":player\")\ninclude(\":player:core\")\ninclude(\":player:mobile\")\ninclude(\":player:shared\")\ninclude(\":player:tv\")\ninclude(\":utils\")\ninclude(\":symbols\")\n"
  },
  {
    "path": "symbols/.gitignore",
    "content": "/build"
  },
  {
    "path": "symbols/build.gradle.kts",
    "content": "plugins {\n    alias(gradleLibs.plugins.google.ksp)\n    alias(gradleLibs.plugins.kotlin.jvm)\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\ndependencies {\n    ksp(libs.material.symbols.compose.ksp)\n    implementation(libs.material.symbols.compose.annotation)\n    compileOnly(androidx.compose.ui)\n}"
  },
  {
    "path": "symbols/src/main/kotlin/dev/aaa1115910/symbols/Symbols.kt",
    "content": "package dev.aaa1115910.symbols\n\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport me.ks.chan.material.symbols.annotation.MaterialSymbol\nimport me.ks.chan.material.symbols.annotation.MaterialSymbolStyle\nimport me.ks.chan.material.symbols.annotation.Style\n\n@MaterialSymbol\ninterface Subtitles {\n    @Style(MaterialSymbolStyle.Rounded)\n    val Rounded: ImageVector\n}\n\n@MaterialSymbol\ninterface SubtitlesOff {\n    @Style(MaterialSymbolStyle.Rounded)\n    val Rounded: ImageVector\n}\n\n@MaterialSymbol\ninterface SubtitlesGear {\n    @Style(MaterialSymbolStyle.Rounded)\n    val Rounded: ImageVector\n}"
  },
  {
    "path": "utils/.gitignore",
    "content": "/build"
  },
  {
    "path": "utils/build.gradle.kts",
    "content": "plugins {\n    alias(gradleLibs.plugins.android.library)\n    alias(gradleLibs.plugins.compose.compiler)\n    alias(gradleLibs.plugins.kotlin.android)\n}\n\nandroid {\n    namespace = \"${AppConfiguration.appId}.utils\"\n    compileSdk = AppConfiguration.compileSdk\n\n    defaultConfig {\n        minSdk = AppConfiguration.minSdk\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        consumerProguardFiles(\"consumer-rules.pro\")\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"r8Test\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n        create(\"alpha\") {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n\n    buildFeatures {\n        compose = true\n        buildConfig = true\n    }\n}\n\njava {\n    toolchain {\n        languageVersion.set(JavaLanguageVersion.of(AppConfiguration.jdk))\n    }\n}\n\ndependencies {\n    implementation(androidx.compose.tv.foundation)\n    implementation(androidx.compose.ui)\n    implementation(androidx.core.ktx)\n    implementation(libs.logging)\n    implementation(libs.material)\n}"
  },
  {
    "path": "utils/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "utils/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "utils/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</manifest>"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/DateExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\nfun Date.formatPubTimeString(): String {\n    val temp = System.currentTimeMillis() - time\n    return when {\n        temp > 1000L * 60 * 60 * 24 -> SimpleDateFormat(\"yyyy-MM-dd\", Locale.getDefault()).format(this)\n        temp > 1000L * 60 * 60 -> \"${temp / (1000 * 60 * 60)}小时前\"\n        temp > 1000L * 60 -> \"${temp / (1000 * 60)}分钟前\"\n        else -> \"刚刚\"\n    }\n}\n"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/Debounce.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.CoroutineScope\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.remember\n\n/**\n * 防抖函数类，用于限制函数的执行频率\n * 在指定的延迟时间内，如果多次调用，只执行最后一次\n * \n * 用法示例：\n * ```kotlin\n * val debouncer = remember { Debouncer(500L) }\n * debouncer.debounce(scope) {\n *     // 执行操作\n * }\n * ```\n * \n * @param delayTime 防抖延迟时间（毫秒）\n */\nclass Debouncer(\n    private val delayTime: Long\n) {\n    private var debounceJob: Job? = null\n\n    /**\n     * 执行防抖操作\n     * \n     * @param scope 协程作用域\n     * @param action 要执行的操作\n     */\n    fun debounce(scope: CoroutineScope, action: suspend () -> Unit) {\n        debounceJob?.cancel()\n        debounceJob = scope.launch {\n            delay(delayTime)\n            action()\n        }\n    }\n\n    /**\n     * 取消当前的防抖任务\n     */\n    fun cancel() {\n        debounceJob?.cancel()\n        debounceJob = null\n    }\n}\n\n/**\n * 创建一个防抖函数\n * \n * 用法示例：\n * ```kotlin\n * val debounceAction = createDebouncer(500L) { scope ->\n *    // 执行操作\n * }\n * debounceAction(scope)\n * ```\n * \n * @param delayTime 防抖延迟时间（毫秒）\n * @param action 要执行的操作\n * @return 返回一个可以被调用的防抖函数\n */\nfun createDebouncer(\n    delayTime: Long,\n    action: suspend () -> Unit\n): (CoroutineScope) -> Unit {\n    val debouncer = Debouncer(delayTime)\n    return { scope ->\n        debouncer.debounce(scope, action)\n    }\n}\n\n/**\n * 带参数的防抖函数\n * \n * 用法示例：\n * ```kotlin\n * val debouncer = remember { ParameterizedDebouncer<String>(500L) }\n * debouncer.debounce(scope, \"parameter\") { parameter ->\n *    // 执行操作\n *    performSearch(parameter)\n * }\n * ```\n * \n * @param T 参数类型\n * @param delayTime 防抖延迟时间（毫秒）\n */\nclass ParameterizedDebouncer<T>(\n    private val delayTime: Long\n) {\n    private var debounceJob: Job? = null\n\n    /**\n     * 执行防抖操作\n     * \n     * @param scope 协程作用域\n     * @param parameter 传递给action的参数\n     * @param action 要执行的操作\n     */\n    fun debounce(scope: CoroutineScope, parameter: T, action: suspend (T) -> Unit) {\n        debounceJob?.cancel()\n        debounceJob = scope.launch {\n            delay(delayTime)\n            action(parameter)\n        }\n    }\n\n    /**\n     * 取消当前的防抖任务\n     */\n    fun cancel() {\n        debounceJob?.cancel()\n        debounceJob = null\n    }\n}\n\n/**\n * 创建一个带参数的防抖函数\n * \n * 用法示例：\n * ```kotlin\n * val debounceAction = createParameterizedDebouncer<String>(500L) { parameter ->\n *    // 执行操作\n *    performSearch(parameter)\n * }\n * debounceAction(scope, \"searchQuery\")\n * ```\n * \n * @param T 参数类型\n * @param delayTime 防抖延迟时间（毫秒）\n * @param action 要执行的操作\n * @return 返回一个可以被调用的防抖函数\n */\nfun <T> createParameterizedDebouncer(\n    delayTime: Long,\n    action: suspend (T) -> Unit\n): (CoroutineScope, T) -> Unit {\n    val debouncer = ParameterizedDebouncer<T>(delayTime)\n    return { scope, parameter ->\n        debouncer.debounce(scope, parameter, action)\n    }\n}\n\n/**\n * Compose 专用的防抖器创建函数，自动处理生命周期管理\n * 在组件销毁时自动清理防抖任务\n * \n * 用法示例：\n * ```kotlin\n * @Composable\n * fun MyScreen() {\n *     val scope = rememberCoroutineScope()\n *     val debouncer = rememberDebouncer<String>(500L)\n *     \n *     debouncer.debounce(scope, \"parameter\") { parameter ->\n *         // 执行操作\n *     }\n * }\n * ```\n * \n * @param T 参数类型\n * @param delayTime 防抖延迟时间（毫秒）\n * @return 返回一个自动管理生命周期的防抖器\n */\n@Composable\nfun <T> rememberDebouncer(delayTime: Long): ParameterizedDebouncer<T> {\n    val debouncer = remember { ParameterizedDebouncer<T>(delayTime) }\n    \n    // 在组件销毁时自动清理\n    DisposableEffect(debouncer) {\n        onDispose {\n            debouncer.cancel()\n        }\n    }\n    \n    return debouncer\n}\n\n/**\n * Compose 专用的简单防抖器创建函数\n * \n * 用法示例：\n * ```kotlin\n * @Composable\n * fun MyScreen() {\n *     val scope = rememberCoroutineScope()\n *     val debouncer = rememberSimpleDebouncer(500L)\n *     \n *     debouncer.debounce(scope) {\n *         // 执行操作\n *     }\n * }\n * ```\n * \n * @param delayTime 防抖延迟时间（毫秒）\n * @return 返回一个自动管理生命周期的防抖器\n */\n@Composable\nfun rememberSimpleDebouncer(delayTime: Long): Debouncer {\n    val debouncer = remember { Debouncer(delayTime) }\n    \n    // 在组件销毁时自动清理\n    DisposableEffect(debouncer) {\n        onDispose {\n            debouncer.cancel()\n        }\n    }\n    \n    return debouncer\n}\n"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/FirebaseUtil.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\n\nobject FirebaseUtil {\n    fun init(context: Context) {}\n    fun log(msg: String) {}\n    fun recordException(throwable: Throwable) {}\n    fun setCrashlyticsCollectionEnabled(enable: Boolean) {}\n}"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/FocusRequesterExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport androidx.compose.ui.focus.FocusRequester\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\n/**\n * 改进的请求焦点的方法，确保线程安全\n * 使用Immediate dispatcher避免不必要的调度延迟\n */\nfun FocusRequester.requestFocus(scope: CoroutineScope) {\n    scope.launch(Dispatchers.Main.immediate) {\n        try {\n            requestFocus()\n        } catch (e: Exception) {\n            // 如果第一次失败，等待一帧再重试\n            delay(16) // 约1帧的时间\n            try {\n                requestFocus()\n            } catch (retryException: Exception) {\n                // 记录日志而不是忽略异常\n                println(\"Focus request failed after retry: ${retryException.message}\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/ImageExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nfun String.resizedImageUrl(size: ImageSize): String {\n    return when (size) {\n        ImageSize.Default -> this\n        else -> \"$this@${size.sizeString}.webp\"\n    }\n}\n\nenum class ImageSize(val sizeString: String) {\n    Default(\"\"),\n    Cover(\"180h_288w_1c\"),\n    SmallVideoCardCover(\"400h_640w_1c\"),\n    SeasonCoverThumbnail(\"466h_622w\"),\n    LargeCover(\"480h_768w_1c\"),\n    Icon(\"100h_100w_1c\"),\n    UgcEpisodeCover(\"384w_216h_1c\")\n}\n"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/KLoggerExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport dev.aaa1115910.bv.utils.BuildConfig\nimport io.github.oshai.kotlinlogging.KLogger\n\nfun KLogger.fInfo(msg: () -> Any?) {\n    info(msg)\n    firebaseLog(\"[Info] ${msg.toStringSafe()}\")\n}\n\nfun KLogger.fWarn(msg: () -> Any?) {\n    warn(msg)\n    firebaseLog(\"[Warn] ${msg.toStringSafe()}\")\n}\n\nfun KLogger.fDebug(msg: () -> Any?) {\n    if (BuildConfig.DEBUG) {\n        info(msg)\n        firebaseLog(\"[Debug] ${msg.toStringSafe()}\")\n    }\n}\n\nfun KLogger.fError(msg: () -> Any?) {\n    error(msg)\n    firebaseLog(\"[Error] ${msg.toStringSafe()}\")\n}\n\nfun KLogger.fException(throwable: Throwable, msg: () -> Any?) {\n    warn { \"$msg: ${throwable.stackTraceToString()}\" }\n    firebaseLog(\"[Exception] ${msg.toStringSafe()}: ${throwable.localizedMessage}\")\n    FirebaseUtil.recordException(throwable)\n}\n\nprivate fun firebaseLog(msg: String) {\n    FirebaseUtil.log(msg)\n}\n\n@Suppress(\"NOTHING_TO_INLINE\")\ninternal inline fun (() -> Any?).toStringSafe(): String {\n    return try {\n        invoke().toString()\n    } catch (e: Exception) {\n        ErrorMessageProducer.getErrorLog(e)\n    }\n}\n\ninternal object ErrorMessageProducer {\n    fun getErrorLog(e: Exception): String {\n        if (System.getProperties().containsKey(\"kotlin-logging.throwOnMessageError\")) {\n            throw e\n        } else {\n            return \"Log message invocation failed: $e\"\n        }\n    }\n}\n"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/KeyEventExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.KeyEventType\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.input.key.type\n\nfun Modifier.onBackPressed(\n    onBackPressed: () -> Unit\n): Modifier = then(\n    Modifier.onKeyEvent {\n        if (it.key == Key.Back) {\n            if (it.type == KeyEventType.KeyUp) onBackPressed()\n            true\n        } else {\n            false\n        }\n    }\n)"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/LongExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport java.util.concurrent.TimeUnit\n\nfun Long.formatHourMinSec(): String {\n    return if (this < 0L) {\n        \"\"\n    } else {\n        val hours = TimeUnit.MILLISECONDS.toHours(this)\n        val minutes = TimeUnit.MILLISECONDS.toMinutes(this) - TimeUnit.HOURS.toMinutes(hours)\n        val seconds = TimeUnit.MILLISECONDS.toSeconds(this) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(this))\n\n        if (hours > 0) {\n            String.format(\"%02d:%02d:%02d\", hours, minutes, seconds)\n        } else {\n            String.format(\"%02d:%02d\", minutes, seconds)\n        }\n    }\n}\n"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/SnapshotStateListExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport androidx.compose.runtime.snapshots.SnapshotStateList\nimport java.util.ArrayDeque\n\n/**\n * 高效地替换列表内容，尽量复用现有对象以减少内存分配\n * @param newList 新列表内容\n */\nfun <T> SnapshotStateList<T>.swapList(newList: List<T>) {\n    if (this.isEmpty()) {\n        // 空列表直接添加全部\n        addAll(newList)\n        return\n    }\n    \n    if (newList.isEmpty()) {\n        // 新列表为空则清空\n        clear()\n        return\n    }\n\n    // 计算需要实际更新的部分\n    val currentSize = this.size\n    val newSize = newList.size\n    val commonSize = minOf(currentSize, newSize)\n    \n    // 1. 更新共同部分（复用已有对象）\n    for (i in 0 until commonSize) {\n        this[i] = newList[i]\n    }\n    \n    // 2. 如果新列表更长，添加额外项\n    if (newSize > currentSize) {\n        addAll(newList.subList(currentSize, newSize))\n    }\n    // 3. 如果旧列表更长，移除多余项\n    else if (currentSize > newSize) {\n        repeat(currentSize - newSize) {\n            this.removeAt(newSize)\n        }\n    }\n}\n"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/Timer.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.os.CountDownTimer\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport java.util.Timer\nimport java.util.TimerTask\n\nfun countDownTimer(\n    millisInFuture: Long,\n    countDownInterval: Long,\n    tag: String,\n    showLogs: Boolean = true,\n    onTick: ((Long) -> Unit)? = null,\n    onComplete: (() -> Unit)? = null\n): CountDownTimer {\n    val logger = KotlinLogging.logger { }\n    val timer = object : CountDownTimer(millisInFuture, countDownInterval) {\n        override fun onTick(millisUntilFinished: Long) {\n            if (showLogs) logger.info { \"[$tag] Count down tick: $millisUntilFinished\" }\n            onTick?.invoke(millisUntilFinished)\n        }\n\n        override fun onFinish() {\n            if (showLogs) logger.info { \"[$tag] Count down finished\" }\n            onComplete?.invoke()\n        }\n    }\n    timer.start()\n    return timer\n}\n\nfun timeTask(\n    delay: Long,\n    period: Long,\n    tag: String,\n    showLogs: Boolean = true,\n    onTick: (() -> Unit)?\n): Timer {\n    val logger = KotlinLogging.logger { }\n    val timer = Timer()\n    timer.schedule(object : TimerTask() {\n        override fun run() {\n            if (showLogs) logger.info { \"[$tag] Time task run\" }\n            onTick?.invoke()\n        }\n    }, delay, period)\n    return timer\n}\n\nfun timeTask(\n    delay: Long,\n    tag: String,\n    showLogs: Boolean = true,\n    onTick: (() -> Unit)?\n): Timer {\n    val logger = KotlinLogging.logger { }\n    val timer = Timer()\n    timer.schedule(object : TimerTask() {\n        override fun run() {\n            if (showLogs) logger.info { \"[$tag] Time task run\" }\n            onTick?.invoke()\n            timer.cancel()\n        }\n    }, delay)\n    return timer\n}"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/ToastExtends.kt",
    "content": "package dev.aaa1115910.bv.util\n\nimport android.content.Context\nimport android.widget.Toast\n\nfun String.toast(context: Context, duration: Int = Toast.LENGTH_SHORT) {\n    Toast.makeText(context, this, duration).show()\n}\n\nfun Int.toast(context: Context, duration: Int = Toast.LENGTH_SHORT) {\n    Toast.makeText(context, context.getText(this), duration).show()\n}"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/createCustomInitialFocusRestorerModifiers.kt",
    "content": "package dev.aaa1115910.bv.util\n\n// Copied from https://cs.android.com/androidx/platform/frameworks/support/+/409d921b5a37ec6857489f327d9cc20141457ab2:tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/createCustomInitialFocusRestorerModifiers.kt\n\n/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusProperties\nimport androidx.compose.ui.focus.focusRequester\n\n/**\n * Assign the parentModifier to the container of items and assign the childModifier to the\n * item that needs to first gain focus. For example, if you want the item at index 0 to get\n * focus for the first time, you can do the following:\n *\n * LazyRow(modifier.then(modifiers.parentModifier) {\n *   item1(modifier.then(modifiers.childModifier) {...}\n *   item2 {...}\n *   item3 {...}\n *   ...\n * }\n */\ndata class FocusRequesterModifiers(\n    val parentModifier: Modifier,\n    val childModifier: Modifier\n)\n\n@Deprecated(\"use Modifier.focusRestorer() instead\")\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun createCustomInitialFocusRestorerModifiers(): FocusRequesterModifiers {\n    val focusRequester = remember { FocusRequester() }\n    val childFocusRequester = remember { FocusRequester() }\n\n    val parentModifier = Modifier\n        .focusRequester(focusRequester)\n        .focusProperties {\n            exit = {\n                focusRequester.saveFocusedChild()\n                FocusRequester.Default\n            }\n            enter = {\n                if (!focusRequester.restoreFocusedChild())\n                    childFocusRequester\n                else\n                    FocusRequester.Cancel\n            }\n        }\n\n    val childModifier = Modifier.focusRequester(childFocusRequester)\n\n    return FocusRequesterModifiers(parentModifier, childModifier)\n}"
  },
  {
    "path": "utils/src/main/kotlin/dev/aaa1115910/bv/util/ifElse.kt",
    "content": "package dev.aaa1115910.bv.util\n\n// Copied from https://cs.android.com/androidx/platform/frameworks/support/+/409d921b5a37ec6857489f327d9cc20141457ab2:tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/ifElse.kt\n\n/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport androidx.compose.ui.Modifier\n\n/**\n * Thanks, Plex 🦄 :)\n */\nfun Modifier.ifElse(\n    condition: () -> Boolean,\n    ifTrueModifier: Modifier,\n    ifFalseModifier: Modifier = Modifier\n): Modifier = then(if (condition()) ifTrueModifier else ifFalseModifier)\n\nfun Modifier.ifElse(\n    condition: Boolean,\n    ifTrueModifier: Modifier,\n    ifFalseModifier: Modifier = Modifier\n): Modifier = ifElse({ condition }, ifTrueModifier, ifFalseModifier)"
  }
]